Coverage Report

Created: 2026-07-13 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/solidity/libyul/optimiser/UnusedStoreEliminator.cpp
Line
Count
Source
1
/*
2
  This file is part of solidity.
3
4
  solidity is free software: you can redistribute it and/or modify
5
  it under the terms of the GNU General Public License as published by
6
  the Free Software Foundation, either version 3 of the License, or
7
  (at your option) any later version.
8
9
  solidity is distributed in the hope that it will be useful,
10
  but WITHOUT ANY WARRANTY; without even the implied warranty of
11
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
  GNU General Public License for more details.
13
14
  You should have received a copy of the GNU General Public License
15
  along with solidity.  If not, see <http://www.gnu.org/licenses/>.
16
*/
17
// SPDX-License-Identifier: GPL-3.0
18
/**
19
 * Optimiser component that removes stores to memory and storage slots that are not used
20
 * or overwritten later on.
21
 */
22
23
#include <libyul/optimiser/UnusedStoreEliminator.h>
24
25
#include <libyul/optimiser/Semantics.h>
26
#include <libyul/optimiser/OptimizerUtilities.h>
27
#include <libyul/optimiser/Semantics.h>
28
#include <libyul/optimiser/SSAValueTracker.h>
29
#include <libyul/optimiser/DataFlowAnalyzer.h>
30
#include <libyul/optimiser/KnowledgeBase.h>
31
#include <libyul/ControlFlowSideEffectsCollector.h>
32
#include <libyul/AST.h>
33
#include <libyul/Utilities.h>
34
35
#include <libyul/backends/evm/EVMDialect.h>
36
37
#include <libsolutil/CommonData.h>
38
39
#include <libevmasm/Instruction.h>
40
#include <libevmasm/SemanticInformation.h>
41
42
#include <range/v3/algorithm/all_of.hpp>
43
44
using namespace solidity;
45
using namespace solidity::yul;
46
47
void UnusedStoreEliminator::run(OptimiserStepContext& _context, Block& _ast)
48
111k
{
49
111k
  std::map<FunctionHandle, SideEffects> functionSideEffects = SideEffectsPropagator::sideEffects(
50
111k
    _context.dialect,
51
111k
    CallGraphGenerator::callGraph(_ast)
52
111k
  );
53
54
111k
  SSAValueTracker ssaValues;
55
111k
  ssaValues(_ast);
56
111k
  std::map<YulName, AssignedValue> values;
57
111k
  for (auto const& [name, expression]: ssaValues.values())
58
18.4M
    values[name] = AssignedValue{expression, {}};
59
60
111k
  bool const ignoreMemory = MSizeFinder::containsMSize(_context.dialect, _ast);
61
111k
  UnusedStoreEliminator rse{
62
111k
    _context.dialect,
63
111k
    functionSideEffects,
64
111k
    ControlFlowSideEffectsCollector{_context.dialect, _ast}.functionSideEffectsNamed(),
65
111k
    values,
66
111k
    ignoreMemory
67
111k
  };
68
111k
  rse(_ast);
69
70
111k
  auto evmDialect = dynamic_cast<EVMDialect const*>(&_context.dialect);
71
111k
  if (evmDialect && evmDialect->providesObjectAccess())
72
107k
    rse.clearActive(Location::Memory);
73
3.88k
  else
74
3.88k
    rse.markActiveAsUsed(Location::Memory);
75
111k
  rse.markActiveAsUsed(Location::Storage);
76
111k
  rse.m_storesToRemove += rse.m_allStores - rse.m_usedStores;
77
78
111k
  std::set<Statement const*> toRemove{rse.m_storesToRemove.begin(), rse.m_storesToRemove.end()};
79
111k
  StatementRemover remover{toRemove};
80
111k
  remover(_ast);
81
111k
}
82
83
UnusedStoreEliminator::UnusedStoreEliminator(
84
  Dialect const& _dialect,
85
  std::map<FunctionHandle, SideEffects> const& _functionSideEffects,
86
  std::map<YulName, ControlFlowSideEffects> _controlFlowSideEffects,
87
  std::map<YulName, AssignedValue> const& _ssaValues,
88
  bool _ignoreMemory
89
):
90
111k
  UnusedStoreBase(_dialect),
91
111k
  m_ignoreMemory(_ignoreMemory),
92
111k
  m_functionSideEffects(_functionSideEffects),
93
111k
  m_controlFlowSideEffects(std::move(_controlFlowSideEffects)),
94
111k
  m_ssaValues(_ssaValues),
95
111k
  m_knowledgeBase(_ssaValues, _dialect)
96
111k
{}
97
98
void UnusedStoreEliminator::operator()(FunctionCall const& _functionCall)
99
6.73M
{
100
6.73M
  UnusedStoreBase::operator()(_functionCall);
101
102
6.73M
  for (Operation const& op: operationsFromFunctionCall(_functionCall))
103
5.14M
    applyOperation(op);
104
105
6.73M
  ControlFlowSideEffects sideEffects;
106
6.73M
  if (auto builtin = resolveBuiltinFunction(_functionCall.functionName, m_dialect))
107
6.22M
    sideEffects = builtin->controlFlowSideEffects;
108
509k
  else
109
509k
  {
110
509k
    yulAssert(std::holds_alternative<Identifier>(_functionCall.functionName));
111
509k
    sideEffects = m_controlFlowSideEffects.at(std::get<Identifier>(_functionCall.functionName).name);
112
509k
  }
113
114
6.73M
  if (sideEffects.canTerminate)
115
78.1k
    markActiveAsUsed(Location::Storage);
116
6.73M
  if (!sideEffects.canContinue)
117
136k
  {
118
136k
    clearActive(Location::Memory);
119
136k
    if (!sideEffects.canTerminate)
120
89.2k
      clearActive(Location::Storage);
121
136k
  }
122
6.73M
}
123
124
void UnusedStoreEliminator::operator()(FunctionDefinition const& _functionDefinition)
125
237k
{
126
237k
  ScopedSaveAndRestore storeOperations(m_storeOperations, {});
127
237k
  UnusedStoreBase::operator()(_functionDefinition);
128
237k
}
129
130
131
void UnusedStoreEliminator::operator()(Leave const&)
132
15.4k
{
133
15.4k
  markActiveAsUsed();
134
15.4k
}
135
136
void UnusedStoreEliminator::visit(Statement const& _statement)
137
26.5M
{
138
26.5M
  using evmasm::Instruction;
139
140
26.5M
  UnusedStoreBase::visit(_statement);
141
142
26.5M
  auto const* exprStatement = std::get_if<ExpressionStatement>(&_statement);
143
26.5M
  if (!exprStatement)
144
23.6M
    return;
145
146
2.90M
  FunctionCall const* funCall = std::get_if<FunctionCall>(&exprStatement->expression);
147
2.90M
  yulAssert(funCall);
148
2.90M
  std::optional<Instruction> instruction = toEVMInstruction(m_dialect, funCall->functionName);
149
2.90M
  if (!instruction)
150
375k
    return;
151
152
5.21M
  if (!ranges::all_of(funCall->arguments, [](Expression const& _expr) -> bool {
153
5.21M
    return std::get_if<Identifier>(&_expr) || std::get_if<Literal>(&_expr);
154
5.21M
  }))
155
0
    return;
156
157
  // We determine if this is a store instruction without additional side-effects
158
  // both by querying a combination of semantic information and by listing the instructions.
159
  // This way the assert below should be triggered on any change.
160
2.52M
  using evmasm::SemanticInformation;
161
2.52M
  bool isStorageWrite = (*instruction == Instruction::SSTORE);
162
2.52M
  bool isMemoryWrite =
163
2.52M
    *instruction == Instruction::EXTCODECOPY ||
164
2.45M
    *instruction == Instruction::CODECOPY ||
165
2.44M
    *instruction == Instruction::CALLDATACOPY ||
166
2.41M
    *instruction == Instruction::RETURNDATACOPY ||
167
    // TODO: Removing MCOPY is complicated because it's not just a store but also a load.
168
    //*instruction == Instruction::MCOPY ||
169
2.40M
    *instruction == Instruction::MSTORE ||
170
1.86M
    *instruction == Instruction::MSTORE8;
171
2.52M
  bool isCandidateForRemoval =
172
2.52M
    *instruction != Instruction::MCOPY &&
173
2.51M
    SemanticInformation::otherState(*instruction) != SemanticInformation::Write && (
174
2.50M
      SemanticInformation::storage(*instruction) == SemanticInformation::Write ||
175
965k
      (!m_ignoreMemory && SemanticInformation::memory(*instruction) == SemanticInformation::Write)
176
2.50M
    );
177
2.52M
  yulAssert(isCandidateForRemoval == (isStorageWrite || (!m_ignoreMemory && isMemoryWrite)));
178
2.52M
  if (isCandidateForRemoval)
179
2.14M
  {
180
2.14M
    if (*instruction == Instruction::RETURNDATACOPY)
181
8.69k
    {
182
      // Out-of-bounds access to the returndata buffer results in a revert,
183
      // so we are careful not to remove a potentially reverting call to a builtin.
184
      // The only way the Solidity compiler uses `returndatacopy` is
185
      // `returndatacopy(X, 0, returndatasize())`, so we only allow to remove this pattern
186
      // (which is guaranteed to never cause an out-of-bounds revert).
187
8.69k
      bool allowReturndatacopyToBeRemoved = false;
188
8.69k
      auto startOffset = identifierNameIfSSA(funCall->arguments.at(1));
189
8.69k
      auto length = identifierNameIfSSA(funCall->arguments.at(2));
190
8.69k
      if (length && startOffset)
191
8.46k
      {
192
8.46k
        FunctionCall const* lengthCall = std::get_if<FunctionCall>(m_ssaValues.at(*length).value);
193
8.46k
        if (
194
8.46k
          m_knowledgeBase.knownToBeZero(*startOffset) &&
195
1.73k
          lengthCall &&
196
1.18k
          toEVMInstruction(m_dialect, lengthCall->functionName) == Instruction::RETURNDATASIZE
197
8.46k
        )
198
829
          allowReturndatacopyToBeRemoved = true;
199
8.46k
      }
200
8.69k
      if (!allowReturndatacopyToBeRemoved)
201
7.86k
        return;
202
8.69k
    }
203
2.14M
    m_allStores.insert(&_statement);
204
2.14M
    std::vector<Operation> operations = operationsFromFunctionCall(*funCall);
205
2.14M
    yulAssert(operations.size() == 1, "");
206
2.14M
    if (operations.front().location == Location::Storage)
207
1.53M
      activeStorageStores().insert(&_statement);
208
603k
    else
209
603k
    {
210
603k
      yulAssert(operations.front().location == Location::Memory, "");
211
603k
      activeMemoryStores().insert(&_statement);
212
603k
    }
213
2.14M
    m_storeOperations[&_statement] = std::move(operations.front());
214
2.14M
  }
215
2.52M
}
216
217
std::vector<UnusedStoreEliminator::Operation> UnusedStoreEliminator::operationsFromFunctionCall(
218
  FunctionCall const& _functionCall
219
) const
220
8.87M
{
221
8.87M
  using evmasm::Instruction;
222
223
8.87M
  SideEffects sideEffects;
224
8.87M
  if (BuiltinFunction const* f = resolveBuiltinFunction(_functionCall.functionName, m_dialect))
225
8.36M
    sideEffects = f->sideEffects;
226
509k
  else
227
509k
  {
228
509k
    yulAssert(std::holds_alternative<Identifier>(_functionCall.functionName));
229
509k
    sideEffects = m_functionSideEffects.at(std::get<Identifier>(_functionCall.functionName).name);
230
509k
  }
231
232
8.87M
  std::optional<Instruction> instruction = toEVMInstruction(m_dialect, _functionCall.functionName);
233
8.87M
  if (!instruction)
234
558k
  {
235
558k
    std::vector<Operation> result;
236
    // Unknown read is worse than unknown write.
237
558k
    if (sideEffects.memory != SideEffects::Effect::None)
238
291k
      result.emplace_back(Operation{Location::Memory, Effect::Read, {}, {}});
239
558k
    if (sideEffects.storage != SideEffects::Effect::None)
240
232k
      result.emplace_back(Operation{Location::Storage, Effect::Read, {}, {}});
241
558k
    return result;
242
558k
  }
243
244
8.31M
  using evmasm::SemanticInformation;
245
246
8.31M
  return util::applyMap(
247
8.31M
    SemanticInformation::readWriteOperations(*instruction),
248
8.31M
    [&](SemanticInformation::Operation const& _op) -> Operation
249
8.31M
    {
250
6.75M
      yulAssert(!(_op.lengthParameter && _op.lengthConstant));
251
6.75M
      yulAssert(_op.effect != Effect::None);
252
6.75M
      Operation ourOp{_op.location, _op.effect, {}, {}};
253
6.75M
      if (_op.startParameter)
254
5.73M
        ourOp.start = identifierNameIfSSA(_functionCall.arguments.at(*_op.startParameter));
255
6.75M
      if (_op.lengthParameter)
256
701k
        ourOp.length = identifierNameIfSSA(_functionCall.arguments.at(*_op.lengthParameter));
257
6.75M
      if (_op.lengthConstant)
258
4.82M
        switch (*_op.lengthConstant)
259
4.82M
        {
260
3.52M
        case 1: ourOp.length = u256(1); break;
261
1.30M
        case 32: ourOp.length = u256(32); break;
262
0
        default: yulAssert(false);
263
4.82M
        }
264
6.75M
      return ourOp;
265
6.75M
    }
266
8.31M
  );
267
8.87M
}
268
269
void UnusedStoreEliminator::applyOperation(UnusedStoreEliminator::Operation const& _operation)
270
5.14M
{
271
5.14M
  std::set<Statement const*>& active =
272
5.14M
    _operation.location == Location::Storage ?
273
2.64M
    activeStorageStores() :
274
5.14M
    activeMemoryStores();
275
276
277
13.5M
  for (auto it = active.begin(); it != active.end();)
278
8.44M
  {
279
8.44M
    Statement const* statement = *it;
280
8.44M
    Operation const& storeOperation = m_storeOperations.at(statement);
281
8.44M
    if (_operation.effect == Effect::Read && !knownUnrelated(storeOperation, _operation))
282
691k
    {
283
      // This store is read from, mark it as used and remove it from the active set.
284
691k
      m_usedStores.insert(statement);
285
691k
      it = active.erase(it);
286
691k
    }
287
7.75M
    else if (_operation.effect == Effect::Write && knownCovered(storeOperation, _operation))
288
      // This store is overwritten before being read, remove it from the active set.
289
1.08M
      it = active.erase(it);
290
6.66M
    else
291
6.66M
      ++it;
292
8.44M
  }
293
5.14M
}
294
295
bool UnusedStoreEliminator::knownUnrelated(
296
  UnusedStoreEliminator::Operation const& _op1,
297
  UnusedStoreEliminator::Operation const& _op2
298
) const
299
1.24M
{
300
1.24M
  if (_op1.location != _op2.location)
301
76.1k
    return true;
302
1.17M
  if (_op1.location == Location::Storage)
303
621k
  {
304
621k
    if (_op1.start && _op2.start)
305
393k
    {
306
393k
      yulAssert(
307
393k
        _op1.length &&
308
393k
        _op2.length &&
309
393k
        lengthValue(*_op1.length) == 1 &&
310
393k
        lengthValue(*_op2.length) == 1
311
393k
      );
312
393k
      return m_knowledgeBase.knownToBeDifferent(*_op1.start, *_op2.start);
313
393k
    }
314
621k
  }
315
551k
  else
316
551k
  {
317
551k
    yulAssert(_op1.location == Location::Memory, "");
318
551k
    if (
319
551k
      (_op1.length && lengthValue(*_op1.length) == 0) ||
320
536k
      (_op2.length && lengthValue(*_op2.length) == 0)
321
551k
    )
322
85.3k
      return true;
323
324
465k
    if (_op1.start && _op1.length && _op2.start)
325
362k
    {
326
362k
      std::optional<u256> length1 = lengthValue(*_op1.length);
327
362k
      std::optional<u256> start1 = m_knowledgeBase.valueIfKnownConstant(*_op1.start);
328
362k
      std::optional<u256> start2 = m_knowledgeBase.valueIfKnownConstant(*_op2.start);
329
362k
      if (
330
362k
        (length1 && start1 && start2) &&
331
195k
        *start1 + *length1 >= *start1 && // no overflow
332
194k
        *start1 + *length1 <= *start2
333
362k
      )
334
75.7k
        return true;
335
362k
    }
336
390k
    if (_op2.start && _op2.length && _op1.start)
337
286k
    {
338
286k
      std::optional<u256> length2 = lengthValue(*_op2.length);
339
286k
      std::optional<u256> start2 = m_knowledgeBase.valueIfKnownConstant(*_op2.start);
340
286k
      std::optional<u256> start1 = m_knowledgeBase.valueIfKnownConstant(*_op1.start);
341
286k
      if (
342
286k
        (length2 && start2 && start1) &&
343
125k
        *start2 + *length2 >= *start2 && // no overflow
344
125k
        *start2 + *length2 <= *start1
345
286k
      )
346
43.5k
        return true;
347
286k
    }
348
349
346k
    if (_op1.start && _op1.length && _op2.start && _op2.length)
350
243k
    {
351
243k
      std::optional<u256> length1 = lengthValue(*_op1.length);
352
243k
      std::optional<u256> length2 = lengthValue(*_op2.length);
353
243k
      if (
354
243k
        (length1 && *length1 <= 32) &&
355
211k
        (length2 && *length2 <= 32) &&
356
96.7k
        m_knowledgeBase.knownToBeDifferentByAtLeast32(*_op1.start, *_op2.start)
357
243k
      )
358
4
        return true;
359
243k
    }
360
346k
  }
361
362
574k
  return false;
363
1.17M
}
364
365
bool UnusedStoreEliminator::knownCovered(
366
  UnusedStoreEliminator::Operation const& _covered,
367
  UnusedStoreEliminator::Operation const& _covering
368
) const
369
7.19M
{
370
7.19M
  if (_covered.location != _covering.location)
371
175k
    return false;
372
7.01M
  if (
373
7.01M
    (_covered.start && _covered.start == _covering.start) &&
374
1.07M
    (_covered.length && _covered.length == _covering.length)
375
7.01M
  )
376
1.03M
    return true;
377
5.98M
  if (_covered.location == Location::Memory)
378
1.41M
  {
379
1.41M
    if (_covered.length && lengthValue(*_covered.length) == 0)
380
9.85k
      return true;
381
382
    // Condition (i = cover_i_ng, e = cover_e_d):
383
    // i.start <= e.start && e.start + e.length <= i.start + i.length
384
1.40M
    if (!_covered.start || !_covering.start || !_covered.length || !_covering.length)
385
61.4k
      return false;
386
1.34M
    std::optional<u256> coveredLength = lengthValue(*_covered.length);
387
1.34M
    std::optional<u256> coveringLength = lengthValue(*_covering.length);
388
1.34M
    if (*_covered.start == *_covering.start)
389
28.9k
      if (coveredLength && coveringLength && *coveredLength <= *coveringLength)
390
4.47k
        return true;
391
1.34M
    std::optional<u256> coveredStart = m_knowledgeBase.valueIfKnownConstant(*_covered.start);
392
1.34M
    std::optional<u256> coveringStart = m_knowledgeBase.valueIfKnownConstant(*_covering.start);
393
1.34M
    if (coveredStart && coveringStart && coveredLength && coveringLength)
394
665k
      if (
395
665k
        *coveringStart <= *coveredStart &&
396
352k
        *coveringStart + *coveringLength >= *coveringStart && // no overflow
397
351k
        *coveredStart + *coveredLength >= *coveredStart && // no overflow
398
349k
        *coveredStart + *coveredLength <= *coveringStart + *coveringLength
399
665k
      )
400
33.2k
        return true;
401
402
    // TODO for this we probably need a non-overflow assumption as above.
403
    // Condition (i = cover_i_ng, e = cover_e_d):
404
    // i.start <= e.start && e.start + e.length <= i.start + i.length
405
1.34M
  }
406
5.87M
  return false;
407
5.98M
}
408
409
void UnusedStoreEliminator::markActiveAsUsed(
410
  std::optional<UnusedStoreEliminator::Location> _onlyLocation
411
)
412
497k
{
413
497k
  if (_onlyLocation == std::nullopt || _onlyLocation == Location::Memory)
414
308k
    for (Statement const* statement: activeMemoryStores())
415
179k
      m_usedStores.insert(statement);
416
497k
  if (_onlyLocation == std::nullopt || _onlyLocation == Location::Storage)
417
493k
    for (Statement const* statement: activeStorageStores())
418
524k
      m_usedStores.insert(statement);
419
497k
  clearActive(_onlyLocation);
420
497k
}
421
422
void UnusedStoreEliminator::clearActive(
423
  std::optional<UnusedStoreEliminator::Location> _onlyLocation
424
)
425
831k
{
426
831k
  if (_onlyLocation == std::nullopt || _onlyLocation == Location::Memory)
427
552k
    activeMemoryStores() = {};
428
831k
  if (_onlyLocation == std::nullopt || _onlyLocation == Location::Storage)
429
583k
    activeStorageStores() = {};
430
831k
}
431
432
std::optional<YulName> UnusedStoreEliminator::identifierNameIfSSA(Expression const& _expression) const
433
6.45M
{
434
6.45M
  if (Identifier const* identifier = std::get_if<Identifier>(&_expression))
435
6.42M
    if (m_ssaValues.count(identifier->name))
436
6.36M
      return {identifier->name};
437
87.2k
  return std::nullopt;
438
6.45M
}