Skip to content
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

Introduce YulNameRepository #15242

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions libyul/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ add_library(yul
YulControlFlowGraphExporter.cpp
YulName.h
YulString.h
YulName.cpp
YulName.h
backends/evm/AbstractAssembly.h
backends/evm/AsmCodeGen.cpp
backends/evm/AsmCodeGen.h
Expand Down
162 changes: 162 additions & 0 deletions libyul/YulName.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
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/YulName.h>

#include <libyul/Exceptions.h>

#include <fmt/compile.h>

namespace solidity::yul
{

YulNameRepository::YulNameRepository()
{
auto const emptyName = defineName("");
yulAssert(emptyName == YulNameRepository::emptyName());
}

std::optional<std::string_view> YulNameRepository::labelOf(YulName _name) const
{
if (!isDerivedName(_name))
{
// if the parent is directly a defined label, we take that one
auto const labelIndex = std::get<0>(m_names[static_cast<size_t>(_name)]);
yulAssert(labelIndex <= std::numeric_limits<size_t>::max());
return m_definedLabels.at(static_cast<size_t>(labelIndex));
}
return std::nullopt;
}

std::string_view YulNameRepository::requiredLabelOf(YulName const& _name) const
{
auto const label = labelOf(_name);
yulAssert(label.has_value(), "YulName currently has no defined label in the YulNameRepository.");
return label.value();
}

YulNameRepository::YulName YulNameRepository::baseNameOf(YulName _name) const
{
while (isDerivedName(_name))
_name = std::get<0>(m_names[static_cast<size_t>(_name)]);
return _name;
}

std::string_view YulNameRepository::baseLabelOf(YulName const& _name) const
{
auto const labelIndex = std::get<0>(m_names[static_cast<size_t>(baseNameOf(_name))]);
yulAssert(labelIndex <= std::numeric_limits<size_t>::max());
return m_definedLabels[static_cast<size_t>(labelIndex)];
}

std::optional<YulNameRepository::YulName> YulNameRepository::nameOfLabel(std::string_view const _label) const
{
auto const it = std::find(m_definedLabels.begin(), m_definedLabels.end(), _label);
if (it != m_definedLabels.end())
{
YulName labelName{static_cast<YulName>(std::distance(m_definedLabels.begin(), it))};
// mostly it'll be iota
if (!isDerivedName(labelName) && std::get<0>(m_names[static_cast<size_t>(labelName)]) == labelName)
return std::get<0>(m_names[static_cast<size_t>(labelName)]);
// if not iota, we have to search
auto itName = std::find(m_names.rbegin(), m_names.rend(), std::make_tuple(labelName, YulNameState::DEFINED));
if (itName != m_names.rend())
return std::get<0>(*itName);
}
return std::nullopt;
}

YulNameRepository::YulName YulNameRepository::defineName(std::string_view const _label)
{
if (auto const name = nameOfLabel(_label); name.has_value())
return *name;

m_definedLabels.emplace_back(_label);
m_names.emplace_back(m_definedLabels.size() - 1, YulNameState::DEFINED);
return static_cast<YulName>(m_names.size() - 1);
}

YulNameRepository::YulName YulNameRepository::deriveName(YulName const& _name)
{
m_names.emplace_back(baseNameOf(_name), YulNameState::DERIVED);
return static_cast<YulName>(m_names.size() - 1);
}

bool YulNameRepository::isDerivedName(YulName const& _name) const
{
yulAssert(_name <= std::numeric_limits<size_t>::max());
return std::get<1>(m_names.at(static_cast<size_t>(_name))) == YulNameState::DERIVED;
}

void YulNameRepository::generateLabels(std::set<YulName> const& _usedNames, std::set<std::string> const& _illegal)
{
std::set<std::string> used;
std::set<YulName> toDerive;
for (auto const name: _usedNames)
{
if (!isDerivedName(name))
{
auto const label = labelOf(name);
yulAssert(label.has_value());
auto const [it, emplaced] = used.emplace(*label);
if (!emplaced || _illegal.count(*it) > 0)
// there's been a clash ,e.g., by calling generate labels twice;
// let's remove this name and derive it instead
toDerive.insert(name);
}
else
yulAssert(isDerivedName(name) || _illegal.count(std::string(*labelOf(name))) == 0);
}

std::vector<std::tuple<std::string, YulName>> generated;
auto namesIt = _usedNames.begin();
for (size_t nameValue = 1; nameValue < m_names.size(); ++nameValue)
{
auto name = static_cast<YulName>(nameValue);
if (namesIt != _usedNames.end() && name == *namesIt)
{
if (isDerivedName(name) || toDerive.find(name) != toDerive.end())
{
std::string const baseLabel(baseLabelOf(name));
std::string label (baseLabel);
size_t bump = 1;
while (used.count(label) > 0 || _illegal.count(label) > 0)
clonker marked this conversation as resolved.
Show resolved Hide resolved
label = fmt::format(FMT_COMPILE("{}_{}"), baseLabel, bump++);
if (auto const existingDefinedName = nameOfLabel(label); existingDefinedName.has_value())
{
std::get<0>(m_names[static_cast<size_t>(name)]) = *existingDefinedName;
std::get<1>(m_names[static_cast<size_t>(name)]) = YulNameState::DEFINED;
}
else
generated.emplace_back(label, name);
used.insert(label);
}
++namesIt;
}
}

for (auto const& [label, name] : generated)
{
yulAssert(_illegal.count(label) == 0);
m_definedLabels.emplace_back(label);
std::get<0>(m_names[static_cast<size_t>(name)]) = static_cast<YulName>(m_definedLabels.size() - 1);
std::get<1>(m_names[static_cast<size_t>(name)]) = YulNameState::DEFINED;
}
}

}
78 changes: 78 additions & 0 deletions libyul/YulName.h
clonker marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,87 @@

#pragma once

#include <libyul/ControlFlowSideEffects.h>
#include <libyul/SideEffects.h>
#include <libyul/YulString.h>

#include <fmt/format.h>

#include <unordered_map>
#include <memory>
#include <vector>
#include <string>
#include <optional>
#include <functional>
#include <numeric>
#include <map>

namespace solidity::yul
{

enum class LiteralKind;
struct Block;

/**
* Yul Name repository.
*
* A database of numerical identifiers for Yul nodes in an AST (`YulName`s). Identifiers can be
*
* - 'defined', i.e., they are equipped with a string label, which can be retrieved by a call to `labelOf(yulName)`;
* - 'derived', i.e., they don't possess a string label but have a parent yul name which is either also derived or
* defined. All dependency chains of derived labels terminate in a defined label.
*
* Such derived identifiers can be introduced, e.g., during certain optimization steps. If the AST (or segments thereof)
* should be printed and/or the string label of identifiers should be retrieved which are still in derived state,
* a call to `generateLabels(identifiers)` changes the status of all derived labels to defined and generates
* unique labels for all identifiers provided to the method. The generated labels are based on their parents.
*/
class YulNameRepository
{
public:
using YulName = std::uint64_t;
YulNameRepository();
/// Defines a new name based on a label. If the label already was defined, it returns the corresponding YulName
/// instead of a new one (`defineName("xyz") == defineName("xyz")`).
YulName defineName(std::string_view _label);

/// Defines a new name based on a parent name. When generating labels, the generated label will be based on the
/// parent's (`deriveName(id) != deriveName(id)`).
YulName deriveName(YulName const& _baseName);

/// The empty name.
static constexpr YulName emptyName() { return 0; }
ekpyron marked this conversation as resolved.
Show resolved Hide resolved

/// Yields the label of a yul name. The name must have been added via `defineName`, a label must have been
/// generated with `generateLabels`, or it is a builtin.
std::optional<std::string_view> labelOf(YulName _name) const;

/// If it can be assumed that the label was already generated, this function will yield it (or fail with an
/// assertion error).
std::string_view requiredLabelOf(YulName const& _name) const;

/// Yields the label of the base name of the provided name. Opposed to `labelOf`, this must always exist.
std::string_view baseLabelOf(YulName const& _name) const;

/// Whether a name is considered derived, i.e., has no label but a parent name.
bool isDerivedName(YulName const& _name) const;

/// Tries to find the label in the defined names and returns the corresponding name.
std::optional<YulName> nameOfLabel(std::string_view label) const;

/// Generates labels for derived names over the set of _usedNames, respecting a set of _illegal labels.
/// This will change the state of all derived names in _usedNames to "not derived" with a label associated to them.
void generateLabels(std::set<YulName> const& _usedNames, std::set<std::string> const& _illegal = {});

private:
enum class YulNameState { DERIVED, DEFINED };

/// Yields the name that the provided name was based on - or the name itself, if the name was directly "defined".
YulName baseNameOf(YulName _name) const;

std::vector<std::string> m_definedLabels{};
std::vector<std::tuple<YulName, YulNameState>> m_names{};
};
using YulName = YulString;

}
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ set(libyul_sources
libyul/SyntaxTest.cpp
libyul/YulInterpreterTest.cpp
libyul/YulInterpreterTest.h
libyul/YulName.cpp
libyul/YulOptimizerTest.cpp
libyul/YulOptimizerTest.h
libyul/YulOptimizerTestCommon.cpp
Expand Down
86 changes: 86 additions & 0 deletions test/libyul/YulName.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
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 <boost/test/unit_test.hpp>

#include <libyul/Dialect.h>
#include <libyul/YulName.h>
#include <libyul/backends/evm/EVMDialect.h>

using namespace solidity;
using namespace solidity::yul;

namespace solidity::yul::test
{

BOOST_AUTO_TEST_SUITE(YulName)

BOOST_AUTO_TEST_CASE(repository_generate_labels_for_derived_types)
{
Dialect dialect{};
YulNameRepository nameRepository;
auto const test1 = nameRepository.defineName("test1");
BOOST_CHECK(!nameRepository.isDerivedName(test1));
auto const test1_1 = nameRepository.deriveName(test1);
BOOST_CHECK(nameRepository.isDerivedName(test1_1));
auto const test1_2 = nameRepository.deriveName(test1);
BOOST_CHECK(nameRepository.isDerivedName(test1_2));
auto const test1_2_1 = nameRepository.deriveName(test1_2);
BOOST_CHECK(nameRepository.isDerivedName(test1_2_1));
auto const test2 = nameRepository.defineName("test2_1");
auto const test2_1 = nameRepository.deriveName(test2);
auto const test3 = nameRepository.defineName("test3");
auto const test3_1 = nameRepository.deriveName(test3);
BOOST_CHECK(test1 != test1_1);
BOOST_CHECK(test1 < test1_1);
nameRepository.generateLabels({test1, test1_1, test1_2, test1_2_1, test2_1, test3, test3_1}, {"test1"});

// marking test1 as invalid means that all labels get bumped up by one
BOOST_CHECK(nameRepository.labelOf(test1) == "test1_1");
BOOST_CHECK(nameRepository.labelOf(test1_1) == "test1_2");
BOOST_CHECK(nameRepository.labelOf(test1_2) == "test1_3");
BOOST_CHECK(nameRepository.labelOf(test1_2_1) == "test1_4");

BOOST_CHECK(nameRepository.labelOf(test2) == "test2_1");
// the label of test2 is reused as it's not in the used names when generating labels
BOOST_CHECK(nameRepository.labelOf(test2_1) == "test2_1");

BOOST_CHECK(nameRepository.labelOf(test3) == "test3");
BOOST_CHECK(nameRepository.labelOf(test3_1) == "test3_1");

// derive a name from the (now labelled) test2_1 name
auto const test2_1_1 = nameRepository.deriveName(test2_1);
// but we have a conflict with an already defined/labelled name, expectation is that we get test2_1_2
auto const conflict = nameRepository.defineName("test2_1_1");
nameRepository.generateLabels({test1, test1_1, test1_2, test1_2_1, test2_1, test2_1_1, test3, test3_1, conflict});
// test2_1 is in the list, so produce a new name
BOOST_CHECK(nameRepository.labelOf(test2_1_1) == "test2_1_2");
BOOST_CHECK(nameRepository.labelOf(conflict) == "test2_1_1");

nameRepository.generateLabels({test2, test2_1, test2_1_1, conflict});
BOOST_CHECK(nameRepository.labelOf(test2) == "test2_1");
// this label gets reassigned, as test2_1 is back in the game
BOOST_CHECK(nameRepository.labelOf(test2_1) == "test2_1_3");
BOOST_CHECK(nameRepository.labelOf(test2_1_1) == "test2_1_2");
BOOST_CHECK(nameRepository.labelOf(conflict) == "test2_1_1");

}

BOOST_AUTO_TEST_SUITE_END()

}