Implement a Function Call Graph

This commit is contained in:
Mathias Baumann 2020-11-18 18:24:33 +01:00 committed by Kamil Śliwak
parent ef147a36b3
commit c977c0cfd0
11 changed files with 422 additions and 5 deletions

View File

@ -18,6 +18,8 @@ set(sources
analysis/DocStringAnalyser.h
analysis/DocStringTagParser.cpp
analysis/DocStringTagParser.h
analysis/FunctionCallGraph.cpp
analysis/FunctionCallGraph.h
analysis/ImmutableValidator.cpp
analysis/ImmutableValidator.h
analysis/GlobalContext.cpp

View File

@ -0,0 +1,207 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/analysis/FunctionCallGraph.h>
using namespace std;
using namespace solidity::frontend;
bool FunctionCallGraphBuilder::CompareByID::operator()(Node const& _lhs, Node const& _rhs) const
{
if (_lhs.index() != _rhs.index())
return _lhs.index() < _rhs.index();
if (std::holds_alternative<SpecialNode>(_lhs))
return std::get<SpecialNode>(_lhs) < std::get<SpecialNode>(_rhs);
return std::get<ASTNode const*>(_lhs)->id() < std::get<ASTNode const*>(_rhs)->id();
}
bool FunctionCallGraphBuilder::CompareByID::operator()(Node const& _lhs, int64_t _rhs) const
{
solAssert(!std::holds_alternative<SpecialNode>(_lhs), "");
return std::get<ASTNode const*>(_lhs)->id() < _rhs;
}
bool FunctionCallGraphBuilder::CompareByID::operator()(int64_t _lhs, Node const& _rhs) const
{
solAssert(!std::holds_alternative<SpecialNode>(_rhs), "");
return _lhs < std::get<ASTNode const*>(_rhs)->id();
}
shared_ptr<FunctionCallGraphBuilder::ContractCallGraph> FunctionCallGraphBuilder::create(ContractDefinition const& _contract)
{
m_contract = &_contract;
m_graph = make_shared<ContractCallGraph>(_contract);
// Create graph for constructor, state vars, etc
m_currentNode = SpecialNode::CreationRoot;
m_currentDispatch = SpecialNode::CreationDispatch;
for (ContractDefinition const* contract: _contract.annotation().linearizedBaseContracts)
visitConstructor(*contract);
m_currentNode.reset();
m_currentDispatch = SpecialNode::InternalDispatch;
// Create graph for all publicly reachable functions
for (auto& [hash, functionType]: _contract.interfaceFunctionList())
{
(void)hash;
if (auto const* funcDef = dynamic_cast<FunctionDefinition const*>(&functionType->declaration()))
if (!m_graph->edges.count(funcDef))
visitCallable(funcDef);
}
// Add all CreationDispatch calls to the RuntimeDispatch as well
for (auto node: m_graph->edges[SpecialNode::CreationDispatch])
add(SpecialNode::InternalDispatch, node);
// Add all external functions to the RuntimeDispatch
for (auto& [hash, functionType]: _contract.interfaceFunctionList())
{
(void)hash;
add(SpecialNode::ExternalDispatch, &functionType->declaration());
}
if (_contract.fallbackFunction())
add(SpecialNode::ExternalDispatch, _contract.fallbackFunction());
if (_contract.receiveFunction())
add(SpecialNode::ExternalDispatch, _contract.receiveFunction());
m_contract = nullptr;
solAssert(!m_currentNode.has_value(), "Current node not properly reset.");
return m_graph;
}
bool FunctionCallGraphBuilder::visit(Identifier const& _identifier)
{
if (auto const* callable = dynamic_cast<CallableDeclaration const*>(_identifier.annotation().referencedDeclaration))
{
solAssert(*_identifier.annotation().requiredLookup == VirtualLookup::Virtual, "");
auto funType = dynamic_cast<FunctionType const*>(_identifier.annotation().type);
// For events kind() == Event, so we have an extra check here
if (funType && funType->kind() == FunctionType::Kind::Internal)
{
processFunction(callable->resolveVirtual(*m_contract), _identifier.annotation());
solAssert(m_currentNode.has_value(), "");
}
}
return true;
}
bool FunctionCallGraphBuilder::visit(NewExpression const& _newExpression)
{
if (ContractType const* contractType = dynamic_cast<ContractType const*>(_newExpression.typeName().annotation().type))
m_graph->createdContracts.emplace(&contractType->contractDefinition());
return true;
}
void FunctionCallGraphBuilder::endVisit(MemberAccess const& _memberAccess)
{
auto functionType = dynamic_cast<FunctionType const*>(_memberAccess.annotation().type);
auto functionDef = dynamic_cast<FunctionDefinition const*>(_memberAccess.annotation().referencedDeclaration);
if (!functionType || !functionDef || functionType->kind() != FunctionType::Kind::Internal)
return;
// Super functions
if (*_memberAccess.annotation().requiredLookup == VirtualLookup::Super)
if (ContractType const* type = dynamic_cast<ContractType const*>(_memberAccess.expression().annotation().type))
{
solAssert(type->isSuper(), "");
functionDef = &functionDef->resolveVirtual(*m_contract, type->contractDefinition().superContract(*m_contract));
}
processFunction(*functionDef, _memberAccess.annotation());
return;
}
void FunctionCallGraphBuilder::endVisit(FunctionCall const& _functionCall)
{
auto* functionType = dynamic_cast<FunctionType const*>(_functionCall.expression().annotation().type);
if (
functionType &&
functionType->kind() == FunctionType::Kind::Internal &&
!functionType->hasDeclaration()
)
add(m_currentDispatch, &_functionCall);
}
void FunctionCallGraphBuilder::visitCallable(CallableDeclaration const* _callable)
{
solAssert(!m_graph->edges.count(_callable), "");
auto previousNode = m_currentNode;
m_currentNode = _callable;
if (previousNode.has_value())
add(*previousNode, _callable);
_callable->accept(*this);
m_currentNode = previousNode;
}
void FunctionCallGraphBuilder::visitConstructor(ContractDefinition const& _contract)
{
for (auto const* stateVar: _contract.stateVariables())
stateVar->accept(*this);
for (auto arg: _contract.baseContracts())
arg->accept(*this);
if (_contract.constructor())
{
add(*m_currentNode, _contract.constructor());
_contract.constructor()->accept(*this);
}
}
bool FunctionCallGraphBuilder::add(Node _caller, ASTNode const* _callee)
{
solAssert(_callee != nullptr, "");
auto result = m_graph->edges.find(_caller);
if (result == m_graph->edges.end())
{
m_graph->edges.emplace(_caller, std::set<ASTNode const*, ASTNode::CompareByID>{_callee});
return true;
}
return result->second.emplace(_callee).second;
}
void FunctionCallGraphBuilder::processFunction(CallableDeclaration const& _callable, ExpressionAnnotation const& _annotation)
{
if (m_graph->edges.count(&_callable))
return;
// Create edge to creation dispatch
if (!_annotation.calledDirectly)
add(m_currentDispatch, &_callable);
visitCallable(&_callable);
}

View File

@ -0,0 +1,98 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/ASTVisitor.h>
#include <vector>
#include <variant>
namespace solidity::frontend
{
/**
* Creates a Function call graph for a contract at the granularity of Solidity
* functions and modifiers
*
* Includes the following special nodes:
* - CreationRoot: All calls made at contract creation originate from this node
* - CreationDispatch: Represents the internal dispatch function at creation time
* - ExternalDispatch: Represents the runtime dispatch for all external functions
* - InternalDispatch: Represents the runtime dispatch for internal function pointers and complex expressions
*
* Nodes are a variant of either the enum SpecialNode or an ASTNode pointer.
* ASTNodes are usually inherited from CallableDeclarations
* (FunctionDefinition, ModifierDefinition, EventDefinition) but for functions
* without declaration it is directly the FunctionCall AST node.
*
* Functions that are not called right away as well as functions without
* declarations have an edge to the internal dispatch node.
*
* Auto-generated getter functions for public state variables are ignored.
*/
class FunctionCallGraphBuilder: private ASTConstVisitor
{
public:
enum class SpecialNode { CreationRoot, CreationDispatch, InternalDispatch, ExternalDispatch };
using Node = std::variant<ASTNode const*, SpecialNode>;
struct CompareByID
{
using is_transparent = void;
bool operator()(Node const& _lhs, Node const& _rhs) const;
bool operator()(Node const& _lhs, int64_t _rhs) const;
bool operator()(int64_t _lhs, Node const& _rhs) const;
};
struct ContractCallGraph
{
ContractCallGraph(ContractDefinition const& _contract): contract(_contract) {}
/// Contract for which this is the graph
ContractDefinition const& contract;
std::map<Node, std::set<ASTNode const*, ASTNode::CompareByID>, CompareByID> edges;
/// Set of contracts created
std::set<ContractDefinition const*, ASTNode::CompareByID> createdContracts;
};
std::shared_ptr<ContractCallGraph> create(ContractDefinition const& _contract);
private:
bool visit(Identifier const& _identifier) override;
bool visit(NewExpression const& _newExpression) override;
void endVisit(MemberAccess const& _memberAccess) override;
void endVisit(FunctionCall const& _functionCall) override;
void visitCallable(CallableDeclaration const* _callable);
void visitConstructor(ContractDefinition const& _contract);
bool add(Node _caller, ASTNode const* _callee);
void processFunction(CallableDeclaration const& _callable, ExpressionAnnotation const& _annotation);
ContractDefinition const* m_contract = nullptr;
std::optional<Node> m_currentNode;
std::shared_ptr<ContractCallGraph> m_graph = nullptr;
Node m_currentDispatch = SpecialNode::CreationDispatch;
};
}

View File

@ -899,7 +899,7 @@ public:
(annotation().contract && annotation().contract->isInterface());
}
FunctionDefinition const& resolveVirtual(
[[nodiscard]] FunctionDefinition const& resolveVirtual(
ContractDefinition const& _mostDerivedContract,
ContractDefinition const* _searchStart = nullptr
) const override;

View File

@ -32,16 +32,29 @@ using namespace solidity::frontend;
string MultiUseYulFunctionCollector::requestedFunctions()
{
string result;
for (auto const& f: m_requestedFunctions)
for (auto const& [name, code]: m_requestedFunctions)
{
solAssert(f.second != "<<STUB<<", "");
(void) name;
solAssert(code != "<<STUB<<", "");
// std::map guarantees ascending order when iterating through its keys.
result += f.second;
result += code;
}
m_requestedFunctions.clear();
return result;
}
set<string> MultiUseYulFunctionCollector::requestedFunctionsNames()
{
set<string> names;
for (auto const& [name, code]: m_requestedFunctions)
{
(void) code;
names.emplace(name);
}
return names;
}
string MultiUseYulFunctionCollector::createFunction(string const& _name, function<string ()> const& _creator)
{
if (!m_requestedFunctions.count(_name))

View File

@ -24,6 +24,7 @@
#include <functional>
#include <map>
#include <set>
#include <string>
namespace solidity::frontend
@ -48,9 +49,11 @@ public:
/// empty return value.
std::string requestedFunctions();
/// Helper function to get the names of all requested functions
std::set<std::string> requestedFunctionsNames();
/// @returns true IFF a function with the specified name has already been collected.
bool contains(std::string const& _name) const { return m_requestedFunctions.count(_name) > 0; }
private:
/// Map from function name to code for a multi-use function.
std::map<std::string, std::string> m_requestedFunctions;

View File

@ -35,6 +35,11 @@ YulArity YulArity::fromType(FunctionType const& _functionType)
string IRNames::function(FunctionDefinition const& _function)
{
if (_function.isConstructor())
return implicitConstructor(*_function.annotation().contract);
// @TODO previously, we had to distinguish creation context and runtime context,
// but since we do not work with jump positions anymore, this should not be a problem, right?
return "fun_" + _function.name() + "_" + to_string(_function.id());
}

View File

@ -48,6 +48,47 @@ using namespace solidity;
using namespace solidity::util;
using namespace solidity::frontend;
namespace {
void verifyCallGraph(set<ASTNode const*, ASTNode::CompareByID> const& _nodes, set<string>& _functionList)
{
for (auto const& node: _nodes)
if (auto const* functionDef = dynamic_cast<FunctionDefinition const*>(node))
solAssert(_functionList.erase(IRNames::function(*functionDef)) == 1, "Function not found in generated code");
static string const funPrefix = "fun_";
for (string const& name: _functionList)
solAssert(name.substr(0, funPrefix.size()) != funPrefix, "Functions found in code gen that were not in the call graph");
}
void collectCalls(FunctionCallGraphBuilder::ContractCallGraph const& _graph, ASTNode const* _root, set<ASTNode const*, ASTNode::CompareByID>& _functions)
{
if (_functions.count(_root) > 0)
return;
set<ASTNode const*, ASTNode::CompareByID> toVisit{_root};
_functions.emplace(_root);
while (!toVisit.empty())
{
ASTNode const* function = *toVisit.begin();
toVisit.erase(toVisit.begin());
auto callees = _graph.edges.find(function);
if (callees == _graph.edges.end())
continue;
for (auto& callee: callees->second)
if (_functions.emplace(callee).second)
toVisit.emplace(callee);
}
}
}
pair<string, string> IRGenerator::run(
ContractDefinition const& _contract,
map<ContractDefinition const*, string_view const> const& _otherYulSources
@ -76,6 +117,29 @@ pair<string, string> IRGenerator::run(
return {warning + ir, warning + asmStack.print()};
}
void IRGenerator::verifyCallGraph(FunctionCallGraphBuilder::ContractCallGraph const& _graph)
{
set<ASTNode const*, ASTNode::CompareByID> functions;
auto collectFromNode = [&](FunctionCallGraphBuilder::SpecialNode _node)
{
auto callees = _graph.edges.find(_node);
if (callees != _graph.edges.end())
for (auto callee: callees->second)
collectCalls(_graph, callee, functions);
};
collectFromNode(FunctionCallGraphBuilder::SpecialNode::CreationRoot);
collectFromNode(FunctionCallGraphBuilder::SpecialNode::CreationDispatch);
::verifyCallGraph(functions, m_creationFunctionList);
functions.clear();
collectFromNode(FunctionCallGraphBuilder::SpecialNode::ExternalDispatch);
collectFromNode(FunctionCallGraphBuilder::SpecialNode::InternalDispatch);
::verifyCallGraph(functions, m_deployedFunctionList);
}
string IRGenerator::generate(
ContractDefinition const& _contract,
map<ContractDefinition const*, string_view const> const& _otherYulSources
@ -144,6 +208,8 @@ string IRGenerator::generate(
generateImplicitConstructors(_contract);
generateQueuedFunctions();
InternalDispatchMap internalDispatchMap = generateInternalDispatchFunctions();
m_creationFunctionList = m_context.functionCollector().requestedFunctionsNames();
t("functions", m_context.functionCollector().requestedFunctions());
t("subObjects", subObjectSources(m_context.subObjectsCreated()));
@ -164,12 +230,14 @@ string IRGenerator::generate(
t("dispatch", dispatchRoutine(_contract));
generateQueuedFunctions();
generateInternalDispatchFunctions();
m_deployedFunctionList = m_context.functionCollector().requestedFunctionsNames();
t("deployedFunctions", m_context.functionCollector().requestedFunctions());
t("deployedSubObjects", subObjectSources(m_context.subObjectsCreated()));
// This has to be called only after all other code generation for the deployed object is complete.
bool deployedInvolvesAssembly = m_context.inlineAssemblySeen();
t("memoryInitDeployed", memoryInit(!deployedInvolvesAssembly));
return t.render();
}

View File

@ -25,6 +25,7 @@
#include <libsolidity/interface/OptimiserSettings.h>
#include <libsolidity/ast/ASTForward.h>
#include <libsolidity/analysis/FunctionCallGraph.h>
#include <libsolidity/codegen/ir/IRGenerationContext.h>
#include <libsolidity/codegen/YulUtilFunctions.h>
#include <liblangutil/EVMVersion.h>
@ -56,6 +57,8 @@ public:
std::map<ContractDefinition const*, std::string_view const> const& _otherYulSources
);
void verifyCallGraph(FunctionCallGraphBuilder::ContractCallGraph const& _graph);
private:
std::string generate(
ContractDefinition const& _contract,
@ -115,6 +118,9 @@ private:
langutil::EVMVersion const m_evmVersion;
OptimiserSettings const m_optimiserSettings;
std::set<std::string> m_creationFunctionList;
std::set<std::string> m_deployedFunctionList;
IRGenerationContext m_context;
YulUtilFunctions m_utils;
};

View File

@ -217,6 +217,7 @@ void CompilerStack::reset(bool _keepSettings)
m_sources.clear();
m_smtlib2Responses.clear();
m_unhandledSMTLib2Queries.clear();
m_contractCallGraphs.clear();
if (!_keepSettings)
{
m_remappings.clear();
@ -399,6 +400,16 @@ bool CompilerStack::analyze()
if (source->ast && !typeChecker.checkTypeRequirements(*source->ast))
noErrors = false;
if (noErrors)
{
FunctionCallGraphBuilder builder;
for (Source const* source: m_sourceOrder)
if (source->ast)
for (ASTPointer<ASTNode> const& node: source->ast->nodes())
if (ContractDefinition* contract = dynamic_cast<ContractDefinition*>(node.get()))
m_contractCallGraphs.emplace(contract, builder.create(*contract));
}
if (noErrors)
{
// Checks that can only be done when all types of all AST nodes are known.
@ -1275,6 +1286,8 @@ void CompilerStack::generateIR(ContractDefinition const& _contract)
IRGenerator generator(m_evmVersion, m_revertStrings, m_optimiserSettings);
tie(compiledContract.yulIR, compiledContract.yulIROptimized) = generator.run(_contract, otherYulSources);
generator.verifyCallGraph(*m_contractCallGraphs[&_contract]);
}
void CompilerStack::generateEVMFromIR(ContractDefinition const& _contract)

View File

@ -24,6 +24,7 @@
#pragma once
#include <libsolidity/analysis/FunctionCallGraph.h>
#include <libsolidity/interface/ReadFile.h>
#include <libsolidity/interface/OptimiserSettings.h>
#include <libsolidity/interface/Version.h>
@ -475,6 +476,7 @@ private:
bool m_generateIR = false;
bool m_generateEwasm = false;
std::map<std::string, util::h160> m_libraries;
std::map<ContractDefinition const*, std::shared_ptr<FunctionCallGraphBuilder::ContractCallGraph> const> m_contractCallGraphs;
/// list of path prefix remappings, e.g. mylibrary: github.com/ethereum = /usr/local/ethereum
/// "context:prefix=target"
std::vector<Remapping> m_remappings;