Skip to content

Commit

Permalink
Yul: introduce YulNameRepository
Browse files Browse the repository at this point in the history
  • Loading branch information
clonker committed Sep 18, 2024
1 parent 154fc1d commit de6ab4d
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 0 deletions.
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)
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
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; }

/// 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()

}

0 comments on commit de6ab4d

Please sign in to comment.