Skip to content

Yul codegen for immutables. #8583

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

Merged
merged 4 commits into from
May 4, 2020
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
33 changes: 26 additions & 7 deletions docs/yul.rst
Original file line number Diff line number Diff line change
Expand Up @@ -885,13 +885,6 @@ the ``dup`` and ``swap`` instructions as well as ``jump`` instructions, labels a
| gaslimit() | | F | block gas limit of the current block |
+-------------------------+-----+---+-----------------------------------------------------------------+

There are three additional functions, ``datasize(x)``, ``dataoffset(x)`` and ``datacopy(t, f, l)``,
which are used to access other parts of a Yul object.

``datasize`` and ``dataoffset`` can only take string literals (the names of other objects)
as arguments and return the size and offset in the data area, respectively.
For the EVM, the ``datacopy`` function is equivalent to ``codecopy``.

.. _yul-call-return-area:

.. note::
Expand All @@ -903,6 +896,32 @@ For the EVM, the ``datacopy`` function is equivalent to ``codecopy``.
The remaining bytes will retain their values as of before the call. If the call fails (it returns ``0``),
nothing is written to that area, but you can still retrieve the failure data using ``returndatacopy``.


In some internal dialects, there are additional functions:

datasize, dataoffset, datacopy
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The functions ``datasize(x)``, ``dataoffset(x)`` and ``datacopy(t, f, l)``,
are used to access other parts of a Yul object.

``datasize`` and ``dataoffset`` can only take string literals (the names of other objects)
as arguments and return the size and offset in the data area, respectively.
For the EVM, the ``datacopy`` function is equivalent to ``codecopy``.


setimmutable, loadimmutable
^^^^^^^^^^^^^^^^^^^^^^^^^^^

The functions ``setimmutable("name", value)`` and ``loadimmutable("name")`` are
used for the immutable mechanism in Solidity and do not nicely map to pur Yul.
The function ``setimmutable`` assumes that the runtime code of a contract
is currently copied to memory at offsot zero. The call to ``setimmutable("name", value)``
will store ``value`` at all points in memory that contain a call to
``loadimmutable("name")``.



.. _yul-object:

Specification of Yul Object
Expand Down
31 changes: 31 additions & 0 deletions libsolidity/codegen/ir/IRGenerationContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#include <libsolidity/codegen/YulUtilFunctions.h>
#include <libsolidity/codegen/ABIFunctions.h>
#include <libsolidity/codegen/CompilerUtils.h>
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/TypeProvider.h>

Expand Down Expand Up @@ -76,6 +77,36 @@ IRVariable const& IRGenerationContext::localVariable(VariableDeclaration const&
return m_localVariables.at(&_varDecl);
}

void IRGenerationContext::registerImmutableVariable(VariableDeclaration const& _variable)
{
solAssert(_variable.immutable(), "Attempted to register a non-immutable variable as immutable.");
solUnimplementedAssert(
_variable.annotation().type->isValueType(),
"Only immutable variables of value type are supported."
);
solAssert(m_reservedMemory.has_value(), "Reserved memory has already been reset.");
m_immutableVariables[&_variable] = CompilerUtils::generalPurposeMemoryStart + *m_reservedMemory;
solAssert(_variable.annotation().type->memoryHeadSize() == 32, "Memory writes might overlap.");
*m_reservedMemory += _variable.annotation().type->memoryHeadSize();
}

size_t IRGenerationContext::immutableMemoryOffset(VariableDeclaration const& _variable) const
{
solAssert(
m_immutableVariables.count(&_variable),
"Unknown immutable variable: " + _variable.name()
);
return m_immutableVariables.at(&_variable);
}

size_t IRGenerationContext::reservedMemory()
{
solAssert(m_reservedMemory.has_value(), "Reserved memory was used before.");
size_t reservedMemory = *m_reservedMemory;
m_reservedMemory = std::nullopt;
return reservedMemory;
}

void IRGenerationContext::addStateVariable(
VariableDeclaration const& _declaration,
u256 _storageOffset,
Expand Down
17 changes: 17 additions & 0 deletions libsolidity/codegen/ir/IRGenerationContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ class IRGenerationContext
bool isLocalVariable(VariableDeclaration const& _varDecl) const { return m_localVariables.count(&_varDecl); }
IRVariable const& localVariable(VariableDeclaration const& _varDecl);

/// Registers an immutable variable of the contract.
/// Should only be called at construction time.
void registerImmutableVariable(VariableDeclaration const& _varDecl);
/// @returns the reserved memory for storing the value of the
/// immutable @a _variable during contract creation.
size_t immutableMemoryOffset(VariableDeclaration const& _variable) const;
/// @returns the reserved memory and resets it to mark it as used.
/// Intended to be used only once for initializing the free memory pointer
/// to after the area used for immutables.
size_t reservedMemory();

void addStateVariable(VariableDeclaration const& _varDecl, u256 _storageOffset, unsigned _byteOffset);
bool isStateVariable(VariableDeclaration const& _varDecl) const { return m_stateVariables.count(&_varDecl); }
std::pair<u256, unsigned> storageLocationOfVariable(VariableDeclaration const& _varDecl) const
Expand Down Expand Up @@ -123,6 +134,12 @@ class IRGenerationContext
OptimiserSettings m_optimiserSettings;
ContractDefinition const* m_mostDerivedContract = nullptr;
std::map<VariableDeclaration const*, IRVariable> m_localVariables;
/// Memory offsets reserved for the values of immutable variables during contract creation.
/// This map is empty in the runtime context.
std::map<VariableDeclaration const*, size_t> m_immutableVariables;
/// Total amount of reserved memory. Reserved memory is used to store
/// immutable variables during contract creation.
std::optional<size_t> m_reservedMemory = {0};
/// Storage offsets of state variables
std::map<VariableDeclaration const*, std::pair<u256, unsigned>> m_stateVariables;
MultiUseYulFunctionCollector m_functions;
Expand Down
78 changes: 63 additions & 15 deletions libsolidity/codegen/ir/IRGenerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ string IRGenerator::generate(
)");

resetContext(_contract);
for (VariableDeclaration const* var: ContractType(_contract).immutableVariables())
m_context.registerImmutableVariable(*var);

t("CreationObject", m_context.creationObjectName(_contract));
t("memoryInit", memoryInit());
Expand Down Expand Up @@ -142,6 +144,7 @@ string IRGenerator::generate(
t("subObjects", subObjectSources(m_context.subObjectsCreated()));

resetContext(_contract);
// Do not register immutables to avoid assignment.
t("RuntimeObject", m_context.runtimeObjectName(_contract));
t("dispatch", dispatchRoutine(_contract));
generateQueuedFunctions();
Expand Down Expand Up @@ -200,7 +203,6 @@ string IRGenerator::generateGetter(VariableDeclaration const& _varDecl)
Type const* type = _varDecl.annotation().type;

solAssert(!_varDecl.isConstant(), "");
solAssert(!_varDecl.immutable(), "");
solAssert(_varDecl.isStateVariable(), "");

if (auto const* mappingType = dynamic_cast<MappingType const*>(type))
Expand Down Expand Up @@ -254,17 +256,32 @@ string IRGenerator::generateGetter(VariableDeclaration const& _varDecl)
solUnimplementedAssert(type->isValueType(), "");

return m_context.functionCollector().createFunction(functionName, [&]() {
pair<u256, unsigned> slot_offset = m_context.storageLocationOfVariable(_varDecl);

return Whiskers(R"(
function <functionName>() -> rval {
rval := <readStorage>(<slot>)
}
)")
("functionName", functionName)
("readStorage", m_utils.readFromStorage(*type, slot_offset.second, false))
("slot", slot_offset.first.str())
.render();
if (_varDecl.immutable())
{
solUnimplementedAssert(type->sizeOnStack() == 1, "");
return Whiskers(R"(
function <functionName>() -> rval {
rval := loadimmutable("<id>")
}
)")
("functionName", functionName)
("id", to_string(_varDecl.id()))
.render();
}
else
{
pair<u256, unsigned> slot_offset = m_context.storageLocationOfVariable(_varDecl);

return Whiskers(R"(
function <functionName>() -> rval {
rval := <readStorage>(<slot>)
}
)")
("functionName", functionName)
("readStorage", m_utils.readFromStorage(*type, slot_offset.second, false))
("slot", slot_offset.first.str())
.render();
}
});
}
}
Expand Down Expand Up @@ -325,7 +342,7 @@ string IRGenerator::initStateVariables(ContractDefinition const& _contract)
{
IRGeneratorForStatements generator{m_context, m_utils};
for (VariableDeclaration const* variable: _contract.stateVariables())
if (!variable->isConstant() && !variable->immutable())
if (!variable->isConstant())
generator.initializeStateVar(*variable);

return generator.code();
Expand Down Expand Up @@ -391,10 +408,41 @@ void IRGenerator::generateImplicitConstructors(ContractDefinition const& _contra
string IRGenerator::deployCode(ContractDefinition const& _contract)
{
Whiskers t(R"X(
<#loadImmutables>
let <var> := mload(<memoryOffset>)
</loadImmutables>

codecopy(0, dataoffset("<object>"), datasize("<object>"))

<#storeImmutables>
setimmutable("<immutableName>", <var>)
Copy link
Member

Choose a reason for hiding this comment

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

I'm still wondering whether this should at some point have a much more general signature, like

setimmutable(0 /* memory offset of code to be modified */, "<object>", "<immutableName>", <var>)

Then immutable references would be an implicit property of yul objects and the semantics would be clearer even if e.g. there's no unique runtime object or a deploy routine that doesn't write code to zero in memory (although I'd not be sure about details, like if the memory offset should be a literal, etc.)...
But then again we might come up with an even better way in the future and I guess it's fine for now (especially since the assembly wouldn't support the extended signature right away).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Referencing the object sounds like a good idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we could come up with a more sophisticated codecopy function that performs the code copy and the immutable replacement at the same time.

Copy link
Member

Choose a reason for hiding this comment

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

I'll create an issue for it.

</storeImmutables>

return(0, datasize("<object>"))
)X");
t("object", m_context.runtimeObjectName(_contract));

vector<map<string, string>> loadImmutables;
vector<map<string, string>> storeImmutables;

for (VariableDeclaration const* immutable: ContractType(_contract).immutableVariables())
{
solUnimplementedAssert(immutable->type()->isValueType(), "");
solUnimplementedAssert(immutable->type()->sizeOnStack() == 1, "");
string yulVar = m_context.newYulVariable();
loadImmutables.emplace_back(map<string, string>{
{"var"s, yulVar},
{"memoryOffset"s, to_string(m_context.immutableMemoryOffset(*immutable))}
});
storeImmutables.emplace_back(map<string, string>{
{"var"s, yulVar},
{"immutableName"s, to_string(immutable->id())}
});
}
t("loadImmutables", std::move(loadImmutables));
// reverse order to ease stack strain
reverse(storeImmutables.begin(), storeImmutables.end());
t("storeImmutables", std::move(storeImmutables));
return t.render();
}

Expand Down Expand Up @@ -489,9 +537,9 @@ string IRGenerator::memoryInit()
// and thus can assume all memory to be zero, including the contents of
// the "zero memory area" (the position CompilerUtils::zeroPointer points to).
return
Whiskers{"mstore(<memPtr>, <generalPurposeStart>)"}
Whiskers{"mstore(<memPtr>, <freeMemoryStart>)"}
("memPtr", to_string(CompilerUtils::freeMemoryPointer))
("generalPurposeStart", to_string(CompilerUtils::generalPurposeMemoryStart))
("freeMemoryStart", to_string(CompilerUtils::generalPurposeMemoryStart + m_context.reservedMemory()))
.render();
}

Expand Down
53 changes: 38 additions & 15 deletions libsolidity/codegen/ir/IRGeneratorForStatements.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,21 @@ string IRGeneratorForStatements::code() const

void IRGeneratorForStatements::initializeStateVar(VariableDeclaration const& _varDecl)
{
solAssert(m_context.isStateVariable(_varDecl), "Must be a state variable.");
solAssert(_varDecl.immutable() || m_context.isStateVariable(_varDecl), "Must be immutable or a state variable.");
solAssert(!_varDecl.isConstant(), "");
solAssert(!_varDecl.immutable(), "");
if (_varDecl.value())
{
_varDecl.value()->accept(*this);
writeToLValue(IRLValue{
*_varDecl.annotation().type,
IRLValue::Storage{
util::toCompactHexWithPrefix(m_context.storageLocationOfVariable(_varDecl).first),
m_context.storageLocationOfVariable(_varDecl).second
}
}, *_varDecl.value());
}
if (!_varDecl.value())
return;

_varDecl.value()->accept(*this);
writeToLValue(
_varDecl.immutable() ?
IRLValue{*_varDecl.annotation().type, IRLValue::Immutable{&_varDecl}} :
IRLValue{*_varDecl.annotation().type, IRLValue::Storage{
util::toCompactHexWithPrefix(m_context.storageLocationOfVariable(_varDecl).first),
m_context.storageLocationOfVariable(_varDecl).second
}},
*_varDecl.value()
);
}

void IRGeneratorForStatements::initializeLocalVar(VariableDeclaration const& _varDecl)
Expand Down Expand Up @@ -1517,8 +1518,12 @@ void IRGeneratorForStatements::handleVariableReference(
// If the value is visited twice, `defineExpression` is called twice on
// the same expression.
solUnimplementedAssert(!_variable.isConstant(), "");
solUnimplementedAssert(!_variable.immutable(), "");
if (m_context.isLocalVariable(_variable))
if (_variable.isStateVariable() && _variable.immutable())
setLValue(_referencingExpression, IRLValue{
*_variable.annotation().type,
IRLValue::Immutable{&_variable}
});
else if (m_context.isLocalVariable(_variable))
setLValue(_referencingExpression, IRLValue{
*_variable.annotation().type,
IRLValue::Stack{m_context.localVariable(_variable)}
Expand Down Expand Up @@ -1939,6 +1944,18 @@ void IRGeneratorForStatements::writeToLValue(IRLValue const& _lvalue, IRVariable
}
},
[&](IRLValue::Stack const& _stack) { assign(_stack.variable, _value); },
[&](IRLValue::Immutable const& _immutable)
{
solUnimplementedAssert(_lvalue.type.isValueType(), "");
solUnimplementedAssert(_lvalue.type.sizeOnStack() == 1, "");
solAssert(_lvalue.type == *_immutable.variable->type(), "");
size_t memOffset = m_context.immutableMemoryOffset(*_immutable.variable);

IRVariable prepared(m_context.newYulVariable(), _lvalue.type);
define(prepared, _value);

m_code << "mstore(" << to_string(memOffset) << ", " << prepared.commaSeparatedList() << ")\n";
},
[&](IRLValue::Tuple const& _tuple) {
auto components = std::move(_tuple.components);
for (size_t i = 0; i < components.size(); i++)
Expand Down Expand Up @@ -1994,6 +2011,12 @@ IRVariable IRGeneratorForStatements::readFromLValue(IRLValue const& _lvalue)
[&](IRLValue::Stack const& _stack) {
define(result, _stack.variable);
},
[&](IRLValue::Immutable const& _immutable) {
solUnimplementedAssert(_lvalue.type.isValueType(), "");
solUnimplementedAssert(_lvalue.type.sizeOnStack() == 1, "");
solAssert(_lvalue.type == *_immutable.variable->type(), "");
define(result) << "loadimmutable(\"" << to_string(_immutable.variable->id()) << "\")\n";
},
[&](IRLValue::Tuple const&) {
solAssert(false, "Attempted to read from tuple lvalue.");
}
Expand Down
8 changes: 6 additions & 2 deletions libsolidity/codegen/ir/IRLValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ struct IRLValue
{
IRVariable variable;
};
struct Immutable
{
VariableDeclaration const* variable = nullptr;
};
struct Storage
{
std::string const slot;
Expand All @@ -59,7 +63,7 @@ struct IRLValue
{
std::vector<std::optional<IRLValue>> components;
};
std::variant<Stack, Storage, Memory, Tuple> kind;
std::variant<Stack, Immutable, Storage, Memory, Tuple> kind;
};

}
}
14 changes: 9 additions & 5 deletions libyul/AsmAnalysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,11 +306,15 @@ vector<YulString> AsmAnalyzer::operator()(FunctionCall const& _funCall)
_funCall.functionName.location,
"Function expects direct literals as arguments."
);
else if (!m_dataNames.count(std::get<Literal>(arg).value))
typeError(
_funCall.functionName.location,
"Unknown data object \"" + std::get<Literal>(arg).value.str() + "\"."
);
else if (
_funCall.functionName.name.str() == "datasize" ||
_funCall.functionName.name.str() == "dataoffset"
)
if (!m_dataNames.count(std::get<Literal>(arg).value))
typeError(
_funCall.functionName.location,
"Unknown data object \"" + std::get<Literal>(arg).value.str() + "\"."
);
}
}
std::reverse(argTypes.begin(), argTypes.end());
Expand Down
5 changes: 5 additions & 0 deletions libyul/backends/evm/AbstractAssembly.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ class AbstractAssembly
virtual void appendDataSize(SubID _sub) = 0;
/// Appends the given data to the assembly and returns its ID.
virtual SubID appendData(bytes const& _data) = 0;

/// Appends loading an immutable variable.
virtual void appendImmutable(std::string const& _identifier) = 0;
/// Appends an assignment to an immutable variable.
virtual void appendImmutableAssignment(std::string const& _identifier) = 0;
};

enum class IdentifierContext { LValue, RValue, VariableDeclaration };
Expand Down
Loading