Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Language Features:
Compiler Features:
* EVM: Support for the EVM version "Prague".
* SMTChecker: Add CHC engine check for underflow and overflow in unary minus operation.
* Yul Optimizer: Reuse optimized Yul IR of contract dependencies instead of optimizing them again along with the dependent contract.


Bugfixes:
Expand Down
31 changes: 29 additions & 2 deletions libsolidity/codegen/ir/IRGenerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
#include <libsolutil/Whiskers.h>
#include <libsolutil/JSON.h>

#include <range/v3/algorithm/count.hpp>

#include <sstream>
#include <variant>

Expand Down Expand Up @@ -92,6 +94,7 @@ std::string IRGenerator::run(
std::map<ContractDefinition const*, std::string_view const> const& _otherYulSources
)
{
m_dependencySubObjects.clear();
return yul::reindent(generate(_contract, _cborMetadata, _otherYulSources));
}

Expand Down Expand Up @@ -181,7 +184,18 @@ std::string IRGenerator::generate(
InternalDispatchMap internalDispatchMap = generateInternalDispatchFunctions(_contract);

t("functions", m_context.functionCollector().requestedFunctions());
t("subObjects", subObjectSources(m_context.subObjectsCreated()));

std::set<ContractDefinition const*, ASTNode::CompareByID> const& creationDependencies = m_context.subObjectsCreated();
t("subObjects", subObjectSources(creationDependencies));
for (ContractDefinition const* dependency: creationDependencies)
{
std::string subObjectPath =
IRNames::creationObject(_contract) + '.' +
IRNames::creationObject(*dependency);
solAssert(ranges::count(subObjectPath, '.') == 1);
solAssert(m_dependencySubObjects.count(subObjectPath) == 0);
m_dependencySubObjects.emplace(subObjectPath, dependency);
}

// This has to be called only after all other code generation for the creation object is complete.
bool creationInvolvesMemoryUnsafeAssembly = m_context.memoryUnsafeInlineAssemblySeen();
Expand All @@ -203,7 +217,20 @@ std::string IRGenerator::generate(
std::set<FunctionDefinition const*> deployedFunctionList = generateQueuedFunctions();
generateInternalDispatchFunctions(_contract);
t("deployedFunctions", m_context.functionCollector().requestedFunctions());
t("deployedSubObjects", subObjectSources(m_context.subObjectsCreated()));

std::set<ContractDefinition const*, ASTNode::CompareByID> const& deployedDependencies = m_context.subObjectsCreated();
t("deployedSubObjects", subObjectSources(deployedDependencies));
for (ContractDefinition const* dependency: deployedDependencies)
{
std::string subObjectPath =
IRNames::creationObject(_contract) + '.' +
IRNames::deployedObject(_contract) + '.' +
IRNames::creationObject(*dependency);
solAssert(ranges::count(subObjectPath, '.') == 2);
solAssert(m_dependencySubObjects.count(subObjectPath) == 0);
m_dependencySubObjects.emplace(subObjectPath, dependency);
}

t("metadataName", yul::Object::metadataName());
t("cborMetadata", util::toHex(_cborMetadata));

Expand Down
4 changes: 4 additions & 0 deletions libsolidity/codegen/ir/IRGenerator.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class IRGenerator
std::map<ContractDefinition const*, std::string_view const> const& _otherYulSources
);

std::map<std::string, ContractDefinition const*> const& dependencySubObjects() const { return m_dependencySubObjects; }

private:
std::string generate(
ContractDefinition const& _contract,
Expand Down Expand Up @@ -145,6 +147,8 @@ class IRGenerator
IRGenerationContext m_context;
YulUtilFunctions m_utils;
OptimiserSettings m_optimiserSettings;

std::map<std::string, ContractDefinition const*> m_dependencySubObjects;
};

}
13 changes: 12 additions & 1 deletion libsolidity/interface/CompilerStack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1519,6 +1519,7 @@ void CompilerStack::generateIR(ContractDefinition const& _contract)
for (auto const& pair: m_contracts)
otherYulSources.emplace(pair.second.contract, pair.second.yulIR);

std::map<std::string, ContractDefinition const*> dependencySubObjects;
if (m_experimentalAnalysis)
{
experimental::IRGenerator generator(
Expand All @@ -1535,6 +1536,7 @@ void CompilerStack::generateIR(ContractDefinition const& _contract)
{}, // TODO: createCBORMetadata(compiledContract, /* _forIR */ true),
otherYulSources
);
// TODO: Use dependencySubObjects() to reuse optimized IR when it becomes available.
}
else
{
Expand All @@ -1552,6 +1554,15 @@ void CompilerStack::generateIR(ContractDefinition const& _contract)
createCBORMetadata(compiledContract, /* _forIR */ true),
otherYulSources
);
dependencySubObjects = generator.dependencySubObjects();
}

std::map<std::string, std::string_view> optimizedSubObjectSources;
for (auto const& [subObjectPath, dependency]: dependencySubObjects)
{
solAssert(optimizedSubObjectSources.count(subObjectPath) == 0);
solAssert(dependency);
optimizedSubObjectSources[subObjectPath] = m_contracts.at(dependency->fullyQualifiedName()).yulIROptimized;
}

yul::YulStack stack(
Expand All @@ -1570,7 +1581,7 @@ void CompilerStack::generateIR(ContractDefinition const& _contract)
);

compiledContract.yulIRAst = stack.astJson();
stack.optimize();
stack.optimize(optimizedSubObjectSources);
compiledContract.yulIROptimized = stack.print(this);
compiledContract.yulIROptimizedAst = stack.astJson();
}
Expand Down
62 changes: 52 additions & 10 deletions libyul/YulStack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

#include <boost/algorithm/string.hpp>

#include <memory>
#include <optional>

using namespace solidity;
Expand Down Expand Up @@ -86,7 +87,7 @@ bool YulStack::parseAndAnalyze(std::string const& _sourceName, std::string const
return analyzeParsed();
}

void YulStack::optimize()
void YulStack::optimize(SourceView const& _optimizedSubObjectSources)
{
yulAssert(m_analysisSuccessful, "Analysis was not successful.");
yulAssert(m_parserResult);
Expand All @@ -99,7 +100,7 @@ void YulStack::optimize()

m_analysisSuccessful = false;
yulAssert(m_parserResult, "");
optimize(*m_parserResult, true);
m_parserResult = optimize(m_parserResult, true, _optimizedSubObjectSources);
yulAssert(analyzeParsed(), "Invalid source code after optimization.");
}

Expand Down Expand Up @@ -150,16 +151,55 @@ void YulStack::compileEVM(AbstractAssembly& _assembly, bool _optimize) const
EVMObjectCompiler::compile(*m_parserResult, _assembly, *dialect, _optimize, m_eofVersion);
}

void YulStack::optimize(Object& _object, bool _isCreation)
std::shared_ptr<yul::Object> YulStack::optimize(
std::shared_ptr<yul::Object> _object,
bool _isCreation,
SourceView const& _optimizedSubObjectSources
)
{
yulAssert(_object.code, "");
yulAssert(_object.analysisInfo, "");
for (auto& subNode: _object.subObjects)
if (auto subObject = dynamic_cast<Object*>(subNode.get()))
yulAssert(_object);
yulAssert(_object->code);
yulAssert(_object->analysisInfo);

std::string objectName = _object->name.str();
if (_optimizedSubObjectSources.size() == 1 && _optimizedSubObjectSources.begin()->first == objectName)
{
YulStack subStack(m_evmVersion, m_eofVersion, m_language, m_optimiserSettings, m_debugInfoSelection);
bool analysisSuccessful = subStack.parseAndAnalyze("", std::string(_optimizedSubObjectSources.begin()->second));
solAssert(analysisSuccessful, "Invalid IR for object " + objectName);

// A few sanity checks that might catch source not matching the object it replaces.
// TODO: Compare names of all subobjects, not just data.
// TODO: Compare debugData by content (ignoring pointers).
yulAssert(subStack.parserResult()->subObjects.size() == _object->subObjects.size());
yulAssert(subStack.parserResult()->qualifiedDataNames() == _object->qualifiedDataNames());
return subStack.parserResult();
}

std::map<std::string, SourceView> subObjectSourceMaps;
for (auto const& [subObjectPath, optimizedIR]: _optimizedSubObjectSources)
{
yulAssert(boost::starts_with(subObjectPath, objectName + '.'));
std::string subObjectPathNoPrefix = subObjectPath.substr(objectName.size() + 1);

std::vector<std::string> pathComponents;
boost::algorithm::split(pathComponents, subObjectPathNoPrefix, boost::is_any_of("."));
yulAssert(pathComponents.size() >= 1);

subObjectSourceMaps.try_emplace(pathComponents[0]);
yulAssert(subObjectSourceMaps[pathComponents[0]].count(subObjectPathNoPrefix) == 0);
subObjectSourceMaps[pathComponents[0]][subObjectPathNoPrefix] = optimizedIR;
}

for (std::shared_ptr<ObjectNode>& subNode: _object->subObjects)
if (auto subObject = std::dynamic_pointer_cast<Object>(subNode))
{
bool isCreation = !boost::ends_with(subObject->name.str(), "_deployed");
optimize(*subObject, isCreation);
std::string subName = subObject->name.str();
bool isCreation = !boost::ends_with(subName, "_deployed");
subNode = optimize(subObject, isCreation, util::valueOrDefault(subObjectSourceMaps, subName));
subObjectSourceMaps.erase(subName);
}
yulAssert(subObjectSourceMaps.empty());

Dialect const& dialect = languageToDialect(m_language, m_evmVersion);
std::unique_ptr<GasMeter> meter;
Expand Down Expand Up @@ -194,14 +234,16 @@ void YulStack::optimize(Object& _object, bool _isCreation)
OptimiserSuite::run(
dialect,
meter.get(),
_object,
*_object,
// Defaults are the minimum necessary to avoid running into "Stack too deep" constantly.
optimizeStackAllocation,
yulOptimiserSteps,
yulOptimiserCleanupSteps,
_isCreation ? std::nullopt : std::make_optional(m_optimiserSettings.expectedExecutionsPerDeployment),
{}
);

return _object;
}

MachineAssemblyObject YulStack::assemble(Machine _machine) const
Expand Down
23 changes: 21 additions & 2 deletions libyul/YulStack.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class YulStack: public langutil::CharStreamProvider
enum class Language { Yul, Assembly, StrictAssembly };
enum class Machine { EVM };

using SourceView = std::map<std::string, std::string_view>;

YulStack():
YulStack(
langutil::EVMVersion{},
Expand Down Expand Up @@ -102,7 +104,20 @@ class YulStack: public langutil::CharStreamProvider

/// Run the optimizer suite. Can only be used with Yul or strict assembly.
/// If the settings (see constructor) disabled the optimizer, nothing is done here.
void optimize();
/// @param _optimizedSubObjectSources A map containing already optimized IR of selected subobjects
/// of the current object.
/// When provided, the corresponding subobjects are not optimized.
/// They are instead completely replaced with the provided source.
/// The keys represent subobject paths within the current object, consisting of dot-deparated object names.
/// The first object on the path must be the current object.
/// All specified subobjects must exist in the current object.
/// Paths may not overlap - if source is provided for a subobject, it may not be
/// provided for any of the objects nested in it at any level.
/// @warning Provided source must be valid and match the unoptimized object, however, aside
/// from some rudimentary sanity checks, this cannot be perfectly enforced.
/// Ensuring that the objects match is the responsibility of the caller and failure to do
/// so may result in semantic differences between optimized and unoptimized code.
void optimize(SourceView const& _optimizedSubObjectSources = {});

/// Run the assembly step (should only be called after parseAndAnalyze).
MachineAssemblyObject assemble(Machine _machine) const;
Expand Down Expand Up @@ -141,7 +156,11 @@ class YulStack: public langutil::CharStreamProvider

void compileEVM(yul::AbstractAssembly& _assembly, bool _optimize) const;

void optimize(yul::Object& _object, bool _isCreation);
std::shared_ptr<yul::Object> optimize(
std::shared_ptr<yul::Object> _object,
bool _isCreation,
SourceView const& _optimizedSubObjectSources = {}
);

Language m_language = Language::Assembly;
langutil::EVMVersion m_evmVersion;
Expand Down