Internals - the Optimizer
The Solidity optimizer operates on assembly, so it can be and also is used by other languages. It splits the sequence of instructions into basic blocks at JUMPs and JUMPDESTs. Inside these blocks, the instructions are analysed and every modification to the stack, to memory or storage is recorded as an expression which consists of an instruction and a list of arguments which are essentially pointers to other expressions. The main idea is now to find expressions that are always equal (on every input) and combine them into an expression class. The optimizer first tries to find each new expression in a list of already known expressions. If this does not work, the expression is simplified according to rules like constant + constant = sum_of_constants or X * 1 = X. Since this is done recursively, we can also apply the latter rule if the second factor is a more complex expression where we know that it will always evaluate to one. Modifications to storage and memory locations have to erase knowledge about storage and memory locations which are not known to be different: If we first write to location x and then to location y and both are input variables, the second could overwrite the first, so we actually do not know what is stored at x after we wrote to y. On the other hand, if a simplification of the expression x - y evaluates to a non-zero constant, we know that we can keep our knowledge about what is stored at x.
At the end of this process, we know which expressions have to be on the stack in the end and have a list of modifications to memory and storage. This information is stored together with the basic blocks and is used to link them. Furthermore, knowledge about the stack, storage and memory configuration is forwarded to the next block(s). If we know the targets of all JUMP and JUMPI instructions, we can build a complete control flow graph of the program. If there is only one target we do not know (this can happen as in principle, jump targets can be computed from inputs), we have to erase all knowledge about the input state of a block as it can be the target of the unknown JUMP. If a JUMPI is found whose condition evaluates to a constant, it is transformed to an unconditional jump.
As the last step, the code in each block is completely re-generated. A dependency graph is created from the expressions on the stack at the end of the block and every operation that is not part of this graph is essentially dropped. Now code is generated that applies the modifications to memory and storage in the order they were made in the original code (dropping modifications which were found not to be needed) and finally, generates all values that are required to be on the stack in the correct place.
These steps are applied to each basic block and the newly generated code is used as replacement if it is smaller. If a basic block is split at a JUMPI and during the analysis, the condition evaluates to a constant, the JUMPI is replaced depending on the value of the constant, and thus code like
Best Answer
Anatomy of a Mapping
A mapping is used to structure value types, such as booleans, integers, addresses, and structs. It consists of two main parts: a
_KeyType
and a_ValueType
; they appear in the following syntax:mapping (_KeyType => _ValueType) mapName
In the example contract provided above,
mapping (uint256 => CampaignData) campaigns
the
uint256
is the_KeyType
and theCampaignData
is the_ValueType
. Note for later that the_ValueType
,CampaignData
, is a struct.Mapping Value Types to Key Types
Think of the
_KeyType
as the key you'll pass through a function to be returned a desired value, or_ValueType
. By default, a mapping is initially empty, so new a_KeyType
will first need to be mapped to a_ValueType
.The example contract's
start
function handles 3 basic processes: (1) giving a_KeyType
to a new_ValueType
CampaignData
struct; (2) populating the newCampaignData
struct with variable values; and (3) procuring a new_KeyType
nextCampaignID
to be ready on deck for the next time the example contract'sstart
function is called. This segments of the function can be dissected like so:(1) giving a
_KeyType
to a new_ValueType
CampaignData
struct:In this line,
nextCampaignId
is mapped as the_KeyType
, and the newcampaign
struct is the_ValueType
.(2) populating the new
CampaignData
struct with variable values:(3) procuring a new
_KeyType
nextCampaignID
for the next time the function is called:Using a mapping here is helpful because a mapping can store many
_KeyTypes
to_ValueTypes
- in this case if there are many campaigns occurring at once they can each have their own campaignID. Each campaign having its own ID is powerful when calling for CampaignData in future functions.Accessing Value Types from a Mapping with Key Types
This example contract actually does not provide any functions that access value types in the mapping. But we can imagine what one might look like: maybe if the deadline of a campaign is extended somehow, an
extendDeadline
function might look like:The
extendDeadline
function would be using thecampaignID
_KeyType
to query thecampaigns
mapping to find the appropriateCampaignData
struct and update itsdeadline
with thenewDeadline
.