Skip to content
Merged
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 @@ -19,6 +19,7 @@ Compiler Features:
* Standard JSON Interface: Do not perform IR optimization when only unoptimized IR is requested.
* Standard JSON Interface: Add ``transientStorageLayout`` output.
* Yul: Drop the deprecated typed Yul dialect that was only accessible via ``--yul`` in the CLI.
* Yul Optimizer: Caching of optimized IR to speed up optimization of contracts with bytecode dependencies.
* Yul Optimizer: The optimizer now treats some previously unrecognized identical literals as identical.
* Commandline Interface: Allow the use of ``--asm-json`` output option in assembler mode to export EVM assembly of the contracts in JSON format.

Expand Down
7 changes: 5 additions & 2 deletions libsolidity/interface/CompilerStack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ static int g_compilerStackCounts = 0;

CompilerStack::CompilerStack(ReadCallback::Callback _readFile):
m_readFile{std::move(_readFile)},
m_objectOptimizer(std::make_shared<yul::ObjectOptimizer>()),
m_errorReporter{m_errorList}
{
// Because TypeProvider is currently a singleton API, we must ensure that
Expand Down Expand Up @@ -1493,7 +1494,8 @@ void CompilerStack::generateIR(ContractDefinition const& _contract, bool _unopti
m_eofVersion,
YulStack::Language::StrictAssembly,
m_optimiserSettings,
m_debugInfoSelection
m_debugInfoSelection,
m_objectOptimizer
);
bool yulAnalysisSuccessful = stack.parseAndAnalyze("", compiledContract.yulIR);
solAssert(
Expand Down Expand Up @@ -1530,7 +1532,8 @@ void CompilerStack::generateEVMFromIR(ContractDefinition const& _contract)
m_eofVersion,
yul::YulStack::Language::StrictAssembly,
m_optimiserSettings,
m_debugInfoSelection
m_debugInfoSelection,
m_objectOptimizer
);
bool analysisSuccessful = stack.parseAndAnalyze("", compiledContract.yulIROptimized);
solAssert(analysisSuccessful);
Expand Down
5 changes: 5 additions & 0 deletions libsolidity/interface/CompilerStack.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
#include <libsolutil/LazyInit.h>
#include <libsolutil/JSON.h>

#include <libyul/ObjectOptimizer.h>

#include <functional>
#include <memory>
#include <ostream>
Expand Down Expand Up @@ -379,6 +381,8 @@ class CompilerStack: public langutil::CharStreamProvider, public evmasm::Abstrac
return VersionIsRelease ? MetadataFormat::WithReleaseVersionTag : MetadataFormat::WithPrereleaseVersionTag;
}

yul::ObjectOptimizer const& objectOptimizer() const { return *m_objectOptimizer; }

private:
/// The state per source unit. Filled gradually during parsing.
struct Source
Expand Down Expand Up @@ -552,6 +556,7 @@ class CompilerStack: public langutil::CharStreamProvider, public evmasm::Abstrac
std::shared_ptr<GlobalContext> m_globalContext;
std::vector<Source const*> m_sourceOrder;
std::map<std::string const, Contract> m_contracts;
std::shared_ptr<yul::ObjectOptimizer> m_objectOptimizer;

langutil::ErrorList m_errorList;
langutil::ErrorReporter m_errorReporter;
Expand Down
2 changes: 2 additions & 0 deletions libyul/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ add_library(yul
FunctionReferenceResolver.h
Object.cpp
Object.h
ObjectOptimizer.cpp
ObjectOptimizer.h
ObjectParser.cpp
ObjectParser.h
Scope.cpp
Expand Down
31 changes: 20 additions & 11 deletions libyul/Object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,6 @@ std::string Object::toString(
yulAssert(hasCode(), "No code");
yulAssert(debugData, "No debug data");

std::string useSrcComment;

if (debugData->sourceNames)
useSrcComment =
"/// @use-src " +
joinHumanReadable(ranges::views::transform(*debugData->sourceNames, [](auto&& _pair) {
return std::to_string(_pair.first) + ":" + util::escapeAndQuoteString(*_pair.second);
})) +
"\n";

std::string inner = "code " + AsmPrinter(
_printingMode,
_dialect,
Expand All @@ -74,7 +64,11 @@ std::string Object::toString(
for (auto const& obj: subObjects)
inner += "\n" + obj->toString(_dialect, _printingMode, _debugInfoSelection, _soliditySourceProvider);

return useSrcComment + "object \"" + name + "\" {\n" + indent(inner) + "\n}";
return
debugData->formatUseSrcComment() +
"object \"" + name + "\" {\n" +
indent(inner) + "\n" +
"}";
}

Json Data::toJson() const
Expand All @@ -85,6 +79,21 @@ Json Data::toJson() const
return ret;
}

std::string ObjectDebugData::formatUseSrcComment() const
{
if (!sourceNames)
return "";

auto formatIdNamePair = [](auto&& _pair) {
return std::to_string(_pair.first) + ":" + util::escapeAndQuoteString(*_pair.second);
};

std::string serializedSourceNames = joinHumanReadable(
ranges::views::transform(*sourceNames, formatIdNamePair)
);
return "/// @use-src " + serializedSourceNames + "\n";
}

Json Object::toJson() const
{
yulAssert(hasCode(), "No code");
Expand Down
6 changes: 4 additions & 2 deletions libyul/Object.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ struct Data: public ObjectNode
struct ObjectDebugData
{
std::optional<SourceNameMap> sourceNames = {};

std::string formatUseSrcComment() const;
};


Expand All @@ -99,9 +101,9 @@ struct Object: public ObjectNode
AsmPrinter::TypePrinting printingMode = AsmPrinter::TypePrinting::Full,
langutil::DebugInfoSelection const& _debugInfoSelection = langutil::DebugInfoSelection::Default(),
langutil::CharStreamProvider const* _soliditySourceProvider = nullptr
) const;
) const override;
/// @returns a compact JSON representation of the AST.
Json toJson() const;
Json toJson() const override;
/// @returns the set of names of data objects accessible from within the code of
/// this object, including the name of object itself
/// Handles all names containing dots as reserved identifiers, not accessible as data.
Expand Down
167 changes: 167 additions & 0 deletions libyul/ObjectOptimizer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
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 <libyul/ObjectOptimizer.h>

#include <libyul/AsmAnalysisInfo.h>
#include <libyul/AsmAnalysis.h>
#include <libyul/AsmPrinter.h>
#include <libyul/AST.h>
#include <libyul/Exceptions.h>
#include <libyul/backends/evm/EVMDialect.h>
#include <libyul/backends/evm/EVMMetrics.h>
#include <libyul/optimiser/ASTCopier.h>
#include <libyul/optimiser/Suite.h>

#include <liblangutil/DebugInfoSelection.h>

#include <libsolutil/Keccak256.h>

#include <boost/algorithm/string.hpp>

#include <limits>
#include <numeric>

using namespace solidity;
using namespace solidity::langutil;
using namespace solidity::util;
using namespace solidity::yul;


Dialect const& yul::languageToDialect(Language _language, EVMVersion _version)
{
switch (_language)
{
case Language::Assembly:
case Language::StrictAssembly:
return EVMDialect::strictAssemblyForEVMObjects(_version);
}
util::unreachable();
}

void ObjectOptimizer::optimize(Object& _object, Settings const& _settings)
{
yulAssert(_object.subId == std::numeric_limits<size_t>::max(), "Not a top-level object.");

optimize(_object, _settings, true /* _isCreation */);
}

void ObjectOptimizer::optimize(Object& _object, Settings const& _settings, bool _isCreation)
{
yulAssert(_object.code());
yulAssert(_object.debugData);

for (auto& subNode: _object.subObjects)
if (auto subObject = dynamic_cast<Object*>(subNode.get()))
{
bool isCreation = !boost::ends_with(subObject->name, "_deployed");
optimize(
*subObject,
_settings,
isCreation
);
}

Dialect const& dialect = languageToDialect(_settings.language, _settings.evmVersion);
std::unique_ptr<GasMeter> meter;
if (EVMDialect const* evmDialect = dynamic_cast<EVMDialect const*>(&dialect))
meter = std::make_unique<GasMeter>(*evmDialect, _isCreation, _settings.expectedExecutionsPerDeployment);

std::optional<h256> cacheKey = calculateCacheKey(_object.code()->root(), *_object.debugData, _settings, _isCreation);
if (cacheKey.has_value() && m_cachedObjects.count(*cacheKey) != 0)
{
overwriteWithOptimizedObject(*cacheKey, _object);
return;
}

OptimiserSuite::run(
dialect,
meter.get(),
_object,
_settings.optimizeStackAllocation,
_settings.yulOptimiserSteps,
_settings.yulOptimiserCleanupSteps,
_isCreation ? std::nullopt : std::make_optional(_settings.expectedExecutionsPerDeployment),
{}
);

if (cacheKey.has_value())
storeOptimizedObject(*cacheKey, _object, dialect);
}

void ObjectOptimizer::storeOptimizedObject(util::h256 _cacheKey, Object const& _optimizedObject, Dialect const& _dialect)
{
m_cachedObjects[_cacheKey] = CachedObject{
std::make_shared<Block>(ASTCopier{}.translate(_optimizedObject.code()->root())),
&_dialect,
};
}

void ObjectOptimizer::overwriteWithOptimizedObject(util::h256 _cacheKey, Object& _object) const
{
yulAssert(m_cachedObjects.count(_cacheKey) != 0);
CachedObject const& cachedObject = m_cachedObjects.at(_cacheKey);

yulAssert(cachedObject.optimizedAST);
_object.setCode(std::make_shared<AST>(ASTCopier{}.translate(*cachedObject.optimizedAST)));
yulAssert(_object.code());

// There's no point in caching AnalysisInfo because it references AST nodes. It can't be shared
// by multiple ASTs and it's easier to recalculate it than properly clone it.
yulAssert(cachedObject.dialect);
_object.analysisInfo = std::make_shared<AsmAnalysisInfo>(
AsmAnalyzer::analyzeStrictAssertCorrect(
*cachedObject.dialect,
_object
)
);

// NOTE: Source name index is included in the key so it must be identical. No need to store and restore it.
}

std::optional<h256> ObjectOptimizer::calculateCacheKey(
Block const& _ast,
ObjectDebugData const& _debugData,
Settings const& _settings,
bool _isCreation
)
{
AsmPrinter asmPrinter(
AsmPrinter::TypePrinting::OmitDefault,
languageToDialect(_settings.language, _settings.evmVersion),
_debugData.sourceNames,
DebugInfoSelection::All()
);

bytes rawKey;
// NOTE: AsmPrinter never prints nativeLocations included in debug data, so ASTs differing only
// in that regard are considered equal here. This is fine because the optimizer does not keep
// them up to date across AST transformations anyway so in any use where they need to be reliable,
// we just regenerate them by reparsing the object.
rawKey += keccak256(asmPrinter(_ast)).asBytes();
Comment on lines +152 to +156
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Note that the way it is now, I am caching the object with wrong nativeLocations, so when it's retrieved from the cache those are still be wrong. I expect that the user of the cache (i.e. YulStack) will reparse the source to correct the problem.

It would be a better to have it encapsulated here, so that the object is reparsed after it's optimized but before it's cached. Unfortunately to do this I'd have to duplicate the parsing and analyzing logic from YulStack here or do some refactor to share it so in the end I thought this was a better trade-off.

rawKey += keccak256(_debugData.formatUseSrcComment()).asBytes();
rawKey += h256(u256(_settings.language)).asBytes();
rawKey += FixedHash<1>(uint8_t(_settings.optimizeStackAllocation ? 0 : 1)).asBytes();
rawKey += h256(u256(_settings.expectedExecutionsPerDeployment)).asBytes();
rawKey += FixedHash<1>(uint8_t(_isCreation ? 0 : 1)).asBytes();
rawKey += keccak256(_settings.evmVersion.name()).asBytes();
rawKey += keccak256(_settings.yulOptimiserSteps).asBytes();
rawKey += keccak256(_settings.yulOptimiserCleanupSteps).asBytes();

return h256(keccak256(rawKey));
}
Loading