Coverage Report

Created: 2026-08-14 07:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/solidity/libyul/optimiser/StackCompressor.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
/**
18
 * Optimisation stage that aggressively rematerializes certain variables in a function to free
19
 * space on the stack until it is compilable.
20
 */
21
22
#include <libyul/optimiser/StackCompressor.h>
23
24
#include <libyul/optimiser/ASTCopier.h>
25
#include <libyul/optimiser/NameCollector.h>
26
#include <libyul/optimiser/Rematerialiser.h>
27
#include <libyul/optimiser/UnusedPruner.h>
28
#include <libyul/optimiser/Metrics.h>
29
#include <libyul/optimiser/Semantics.h>
30
31
#include <libyul/backends/evm/ControlFlowGraphBuilder.h>
32
#include <libyul/backends/evm/StackHelpers.h>
33
#include <libyul/backends/evm/StackLayoutGenerator.h>
34
35
#include <libyul/AsmAnalysis.h>
36
#include <libyul/AsmAnalysisInfo.h>
37
38
#include <libyul/CompilabilityChecker.h>
39
40
#include <libyul/AST.h>
41
42
#include <libsolutil/CommonData.h>
43
44
using namespace solidity;
45
using namespace solidity::yul;
46
47
namespace
48
{
49
50
/**
51
 * Class that discovers all variables that can be fully eliminated by rematerialization,
52
 * and the corresponding approximate costs.
53
 *
54
 * Prerequisite: Disambiguator, Function Grouper
55
 */
56
class RematCandidateSelector: public DataFlowAnalyzer
57
{
58
public:
59
23.8k
  explicit RematCandidateSelector(Dialect const& _dialect): DataFlowAnalyzer(_dialect, MemoryAndStorage::Ignore) {}
60
61
  /// @returns a map from function name to rematerialisation costs to a vector of variables to rematerialise
62
  /// and variables that occur in their expression.
63
  /// While the map is sorted by cost, the contained vectors are sorted by the order of occurrence.
64
  std::map<YulName, std::map<size_t, std::vector<YulName>>> candidates()
65
23.8k
  {
66
23.8k
    std::map<YulName, std::map<size_t, std::vector<YulName>>> cand;
67
23.8k
    for (auto const& [functionName, candidate]: m_candidates)
68
188k
    {
69
188k
      if (size_t const* cost = util::valueOrNullptr(m_expressionCodeCost, candidate))
70
132k
      {
71
132k
        size_t numRef = m_numReferences[candidate];
72
132k
        cand[functionName][*cost * numRef].emplace_back(candidate);
73
132k
      }
74
188k
    }
75
23.8k
    return cand;
76
23.8k
  }
77
78
  using DataFlowAnalyzer::operator();
79
  void operator()(FunctionDefinition& _function) override
80
87.1k
  {
81
87.1k
    yulAssert(m_currentFunctionName.empty());
82
87.1k
    m_currentFunctionName = _function.name;
83
87.1k
    DataFlowAnalyzer::operator()(_function);
84
87.1k
    m_currentFunctionName = {};
85
87.1k
  }
86
87
  void operator()(VariableDeclaration& _varDecl) override
88
473k
  {
89
473k
    DataFlowAnalyzer::operator()(_varDecl);
90
473k
    if (_varDecl.variables.size() == 1)
91
436k
    {
92
436k
      YulName varName = _varDecl.variables.front().name;
93
436k
      if (AssignedValue const* value = variableValue(varName))
94
188k
      {
95
188k
        yulAssert(!m_expressionCodeCost.count(varName), "");
96
188k
        m_candidates.emplace_back(m_currentFunctionName, varName);
97
188k
        m_expressionCodeCost[varName] = CodeCost::codeCost(m_dialect, *value->value);
98
188k
      }
99
436k
    }
100
473k
  }
101
102
  void operator()(Assignment& _assignment) override
103
116k
  {
104
116k
    for (auto const& var: _assignment.variableNames)
105
116k
      rematImpossible(var.name);
106
116k
    DataFlowAnalyzer::operator()(_assignment);
107
116k
  }
108
109
  // We use visit(Expression) because operator()(Identifier) would also
110
  // get called on left-hand-sides of assignments.
111
  void visit(Expression& _e) override
112
8.58M
  {
113
8.58M
    if (std::holds_alternative<Identifier>(_e))
114
828k
    {
115
828k
      YulName name = std::get<Identifier>(_e).name;
116
828k
      if (m_expressionCodeCost.count(name))
117
305k
      {
118
305k
        if (!variableValue(name))
119
37.8k
          rematImpossible(name);
120
267k
        else
121
267k
          ++m_numReferences[name];
122
305k
      }
123
828k
    }
124
8.58M
    DataFlowAnalyzer::visit(_e);
125
8.58M
  }
126
127
  /// Remove the variable from the candidate set.
128
  void rematImpossible(YulName _variable)
129
154k
  {
130
154k
    m_numReferences.erase(_variable);
131
154k
    m_expressionCodeCost.erase(_variable);
132
154k
  }
133
134
  YulName m_currentFunctionName = {};
135
136
  /// All candidate variables by function name, in order of occurrence.
137
  std::vector<std::pair<YulName, YulName>> m_candidates;
138
  /// Candidate variables and the code cost of their value.
139
  std::map<YulName, size_t> m_expressionCodeCost;
140
  /// Number of references to each candidate variable.
141
  std::map<YulName, size_t> m_numReferences;
142
};
143
144
/// Selects at most @a _numVariables among @a _candidates.
145
std::set<YulName> chooseVarsToEliminate(
146
  std::map<size_t, std::vector<YulName>> const& _candidates,
147
  size_t _numVariables
148
)
149
51.1k
{
150
51.1k
  std::set<YulName> varsToEliminate;
151
51.1k
  for (auto&& [cost, candidates]: _candidates)
152
10.6k
    for (auto&& candidate: candidates)
153
12.8k
    {
154
12.8k
      if (varsToEliminate.size() >= _numVariables)
155
1.27k
        return varsToEliminate;
156
11.5k
      varsToEliminate.insert(candidate);
157
11.5k
    }
158
49.8k
  return varsToEliminate;
159
51.1k
}
160
161
void eliminateVariables(
162
  Dialect const& _dialect,
163
  Block& _ast,
164
  std::map<YulName, int> const& _numVariables,
165
  bool _allowMSizeOptimization
166
)
167
18.7k
{
168
18.7k
  RematCandidateSelector selector{_dialect};
169
18.7k
  selector(_ast);
170
18.7k
  std::map<YulName, std::map<size_t, std::vector<YulName>>> candidates = selector.candidates();
171
172
18.7k
  std::set<YulName> varsToEliminate;
173
18.7k
  for (auto const& [functionName, numVariables]: _numVariables)
174
51.1k
  {
175
51.1k
    yulAssert(numVariables > 0);
176
51.1k
    varsToEliminate += chooseVarsToEliminate(candidates[functionName], static_cast<size_t>(numVariables));
177
51.1k
  }
178
179
18.7k
  Rematerialiser::run(_dialect, _ast, std::move(varsToEliminate));
180
  // Do not remove functions.
181
18.7k
  std::set<YulName> allFunctions = NameCollector{_ast, NameCollector::OnlyFunctions}.names();
182
18.7k
  UnusedPruner::runUntilStabilised(_dialect, _ast, _allowMSizeOptimization, nullptr, allFunctions);
183
18.7k
}
184
185
void eliminateVariablesOptimizedCodegen(
186
  Dialect const& _dialect,
187
  Block& _ast,
188
  std::map<YulName, std::vector<StackLayoutGenerator::StackTooDeep>> const& _unreachables,
189
  bool _allowMSizeOptimization
190
)
191
87.8k
{
192
88.5k
  if (std::all_of(_unreachables.begin(), _unreachables.end(), [](auto const& _item) { return _item.second.empty(); }))
193
82.7k
    return;
194
195
5.07k
  RematCandidateSelector selector{_dialect};
196
5.07k
  selector(_ast);
197
198
5.07k
  std::map<YulName, size_t> candidates;
199
5.07k
  for (auto const& [functionName, candidatesInFunction]: selector.candidates())
200
19.6k
    for (auto [cost, candidatesWithCost]: candidatesInFunction)
201
52.1k
      for (auto candidate: candidatesWithCost)
202
88.8k
        candidates[candidate] = cost;
203
204
5.07k
  std::set<YulName> varsToEliminate;
205
206
  // TODO: this currently ignores the fact that variables may reference other variables we want to eliminate.
207
5.07k
  for (auto const& [functionName, unreachables]: _unreachables)
208
16.3k
    for (auto const& unreachable: unreachables)
209
206k
    {
210
206k
      std::map<size_t, std::vector<YulName>> suitableCandidates;
211
206k
      size_t neededSlots = unreachable.deficit;
212
206k
      for (auto varName: unreachable.variableChoices)
213
3.11M
      {
214
3.11M
        if (varsToEliminate.count(varName))
215
983k
          --neededSlots;
216
2.13M
        else if (size_t* cost = util::valueOrNullptr(candidates, varName))
217
107k
          if (!util::contains(suitableCandidates[*cost], varName))
218
107k
            suitableCandidates[*cost].emplace_back(varName);
219
3.11M
      }
220
206k
      for (auto candidatesByCost: suitableCandidates)
221
48.9k
      {
222
48.9k
        for (auto candidate: candidatesByCost.second)
223
68.7k
          if (neededSlots--)
224
56.5k
            varsToEliminate.emplace(candidate);
225
12.1k
          else
226
12.1k
            break;
227
48.9k
        if (!neededSlots)
228
9.44k
          break;
229
48.9k
      }
230
206k
    }
231
5.07k
  Rematerialiser::run(_dialect, _ast, std::move(varsToEliminate), true);
232
  // Do not remove functions.
233
5.07k
  std::set<YulName> allFunctions = NameCollector{_ast, NameCollector::OnlyFunctions}.names();
234
5.07k
  UnusedPruner::runUntilStabilised(_dialect, _ast, _allowMSizeOptimization, nullptr, allFunctions);
235
5.07k
}
236
237
}
238
239
std::tuple<bool, Block> StackCompressor::run(
240
  Object const& _object,
241
  bool _optimizeStackAllocation,
242
  size_t _maxIterations)
243
116k
{
244
116k
  yulAssert(_object.hasCode());
245
116k
  yulAssert(_object.dialect(), "No dialect");
246
116k
  yulAssert(
247
116k
    !_object.code()->root().statements.empty() && std::holds_alternative<Block>(_object.code()->root().statements.at(0)),
248
116k
    "Need to run the function grouper before the stack compressor."
249
116k
  );
250
116k
  bool usesOptimizedCodeGenerator = false;
251
116k
  auto evmDialect = dynamic_cast<EVMDialect const*>(_object.dialect());
252
116k
  if (evmDialect)
253
116k
  {
254
116k
    usesOptimizedCodeGenerator =
255
116k
      _optimizeStackAllocation &&
256
116k
      evmDialect->evmVersion().canOverchargeGasForCall() &&
257
92.4k
      evmDialect->providesObjectAccess();
258
116k
  }
259
116k
  bool allowMSizeOptimization = !MSizeFinder::containsMSize(*_object.dialect(), _object.code()->root());
260
116k
  Block astRoot = std::get<Block>(ASTCopier{}(_object.code()->root()));
261
116k
  if (usesOptimizedCodeGenerator)
262
87.8k
  {
263
87.8k
    yul::AsmAnalysisInfo analysisInfo = yul::AsmAnalyzer::analyzeStrictAssertCorrect(
264
87.8k
      *_object.dialect(),
265
87.8k
      astRoot,
266
87.8k
      _object.summarizeStructure()
267
87.8k
    );
268
87.8k
    std::unique_ptr<CFG> cfg = ControlFlowGraphBuilder::build(analysisInfo, *_object.dialect(), astRoot);
269
87.8k
    yulAssert(evmDialect);
270
87.8k
    eliminateVariablesOptimizedCodegen(
271
87.8k
      *_object.dialect(),
272
87.8k
      astRoot,
273
87.8k
      StackLayoutGenerator::reportStackTooDeep(*cfg, *evmDialect),
274
87.8k
      allowMSizeOptimization
275
87.8k
    );
276
87.8k
  }
277
28.4k
  else
278
28.4k
  {
279
47.2k
    for (size_t iterations = 0; iterations < _maxIterations; iterations++)
280
46.0k
    {
281
46.0k
      Object object(_object);
282
46.0k
      object.setCode(std::make_shared<AST>(*_object.dialect(), std::get<Block>(ASTCopier{}(astRoot))));
283
46.0k
      std::map<YulName, int> stackSurplus = CompilabilityChecker(object, _optimizeStackAllocation).stackDeficit;
284
46.0k
      if (stackSurplus.empty())
285
27.2k
        return std::make_tuple(true, std::move(astRoot));
286
18.7k
      eliminateVariables(
287
18.7k
        *object.dialect(),
288
18.7k
        astRoot,
289
18.7k
        stackSurplus,
290
18.7k
        allowMSizeOptimization
291
18.7k
      );
292
18.7k
    }
293
28.4k
  }
294
88.9k
  return std::make_tuple(false, std::move(astRoot));
295
116k
}
296