Coverage Report

Created: 2026-07-13 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/solidity/libyul/backends/evm/ssa/StackShuffler.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
#include <libyul/backends/evm/ssa/StackShuffler.h>
20
21
#include <range/v3/algorithm/count.hpp>
22
#include <range/v3/view/enumerate.hpp>
23
24
using namespace solidity::yul::ssa;
25
using namespace solidity::yul::ssa::detail;
26
27
Target::Target(
28
  StackData const& _args,
29
  StackSlotLiveness const& _liveOut,
30
  std::size_t const _targetSize,
31
  spill::SpillSet const* _spilledVariables
32
):
33
0
  args(_args),
34
0
  liveOut(_liveOut),
35
0
  size(_targetSize),
36
0
  tailSize(_targetSize - _args.size())
37
0
{
38
0
  minCount.reserve(_args.size() + _liveOut.size());
39
0
  for (auto const& arg: _args)
40
0
    if (!arg.isJunk() && !slotIsSpilled(arg, _spilledVariables))
41
0
      ++minCount[arg];
42
0
  for (auto const& liveSlot: _liveOut | ranges::views::keys)
43
0
    if (!slotIsSpilled(liveSlot, _spilledVariables))
44
0
      ++minCount[liveSlot];
45
0
}
46
47
State::State(StackData const& _stackData, Target const& _target, spill::SpillSet const* const _spilledVariables, std::size_t const _reachableStackDepth):
48
0
  m_stackData(_stackData),
49
0
  m_target(_target),
50
0
  m_spilledVariables(_spilledVariables),
51
0
  m_reachableStackDepth(_reachableStackDepth)
52
0
{
53
0
  m_histogram.reserve(_stackData.size());
54
0
  m_histogramReachable.reserve(_stackData.size());
55
0
  m_histogramTail.reserve(_stackData.size());
56
0
  m_histogramArgs.reserve(_target.args.size());
57
0
  for (auto const& [i, slot]: _stackData | ranges::views::enumerate)
58
0
  {
59
    // we don't care about junk in the tail
60
0
    if (i < _target.tailSize && slot.isJunk())
61
0
      continue;
62
    // we purposefully skip over junk in the target args as they are always 'correct'
63
0
    if (i >= _target.tailSize && i < _target.size && _target.args[i - _target.tailSize].isJunk())
64
0
      continue;
65
66
0
    ++m_histogram[slot];
67
0
    if (i < _target.tailSize)
68
0
      ++m_histogramTail[slot];
69
0
    else if (i < _target.size)
70
0
      ++m_histogramArgs[slot];
71
0
    if (_stackData.size() - i - 1 < _reachableStackDepth)
72
0
      ++m_histogramReachable[slot];
73
0
  }
74
0
}
75
76
std::size_t State::size() const
77
0
{
78
0
  return m_stackData.size();
79
0
}
80
81
std::size_t State::count(StackSlot const& _slot) const
82
0
{
83
0
  return solidity::util::valueOrDefault(m_histogram, _slot, static_cast<size_t>(0));
84
0
}
85
86
std::size_t State::countInArgs(StackSlot const& _slot) const
87
0
{
88
0
  return solidity::util::valueOrDefault(m_histogramArgs, _slot, static_cast<size_t>(0));
89
0
}
90
91
std::size_t State::countInTail(StackSlot const& _slot) const
92
0
{
93
0
  return solidity::util::valueOrDefault(m_histogramTail, _slot, static_cast<size_t>(0));
94
0
}
95
96
std::size_t State::countReachable(StackSlot const& _slot) const
97
0
{
98
0
  return solidity::util::valueOrDefault(m_histogramReachable, _slot, static_cast<size_t>(0));
99
0
}
100
101
std::size_t State::targetMinCount(StackSlot const& _slot) const
102
0
{
103
0
  return solidity::util::valueOrDefault(m_target.minCount, _slot, static_cast<size_t>(0));
104
0
}
105
106
std::size_t State::targetArgsCount(StackSlot const& _slot) const
107
0
{
108
0
  return static_cast<size_t>(ranges::count(m_target.args, _slot));
109
0
}
110
111
bool State::admissible() const
112
0
{
113
0
  if (m_target.size != m_stackData.size())
114
0
    return false;
115
116
  // check if the args are correct
117
0
  for (size_t i = 0; i < m_target.args.size(); ++i)
118
0
    if (!isArgsCompatible(StackOffset{m_stackData.size() - i - 1}, StackOffset{m_stackData.size() - i - 1}))
119
0
      return false;
120
121
  // check if the distribution is correct (implying that the stack is admissible as JUNK target args are not counted)
122
0
  for (auto const& [targetSlot, targetMinCount]: m_target.minCount)
123
0
    if (count(targetSlot) < targetMinCount)
124
0
      return false;
125
0
  return true;
126
0
}
127
128
bool State::requiredInArgs(StackSlot const& _slot) const
129
0
{
130
0
  return ranges::find(m_target.args, _slot) != ranges::end(m_target.args);
131
0
}
132
133
bool State::requiredInTail(StackSlot const& _slot) const
134
0
{
135
0
  if (!_slot.isValue() || !m_target.liveOut.contains(_slot))
136
0
    return false;
137
  // Spilled values can be rematerialized, so they need not occupy a tail slot.
138
0
  return !slotIsSpilled(_slot);
139
0
}
140
141
bool State::offsetInTargetArgsRegion(StackOffset const _offset) const
142
0
{
143
0
  return _offset.value >= m_target.tailSize && _offset.value < m_target.size;
144
0
}
145
146
StackSlot const& State::targetArg(StackOffset const _targetOffset) const
147
0
{
148
0
  yulAssert(offsetInTargetArgsRegion(_targetOffset));
149
0
  return m_target.args[_targetOffset.value - m_target.tailSize];
150
0
}
151
152
bool State::isArgsCompatible(StackOffset const _sourceOffset, StackOffset const _targetOffset) const
153
0
{
154
0
  if (_sourceOffset >= m_stackData.size() || !offsetInTargetArgsRegion(_targetOffset))
155
0
    return false;
156
0
  auto const& arg = targetArg(_targetOffset);
157
0
  return arg.isJunk() || m_stackData[_sourceOffset.value] == arg;
158
0
}
159
160
bool State::targetArbitrary(StackOffset const _targetOffset) const
161
0
{
162
0
  return targetArg(_targetOffset).isJunk();
163
0
}
164
165
bool State::isSourceCompatible(StackOffset const _sourceOffset1, StackOffset const _sourceOffset2) const
166
0
{
167
0
  return _sourceOffset1 < m_stackData.size() &&
168
0
    _sourceOffset2 < m_stackData.size() &&
169
0
    m_stackData[_sourceOffset1.value] == m_stackData[_sourceOffset2.value];
170
0
}
171
172
bool State::isSafeToSwapWithTop(StackOffset const _offset) const
173
0
{
174
0
  auto const& top = m_stackData.back();
175
0
  yulAssert(_offset.value < size());
176
0
  auto const& slot = m_stackData[_offset.value];
177
0
  return !isArgsCompatible(_offset, _offset) && // the offset isn't already in the right position wrt args
178
0
    !isArgsCompatible(StackOffset{size() - 1}, StackOffset{size() - 1}) && // the top isn't already in the right position wrt args
179
0
    (
180
0
      !requiredInArgs(top) || // current top can go into tail, ie it's not required as arg or
181
0
      countReachable(top) > 1 // there's more of it in reachable stack depth
182
0
    ) &&
183
0
    (
184
0
      target().tailSize <= _offset.value ||  // sourceOffset not in tail
185
0
      !requiredInTail(slot) ||  // we're in tail but sourceOffset not needed in tail
186
0
      (countInTail(slot) > 1 && requiredInTail(slot))  // swapping source offset away from tail doesn't decrease tail correctness
187
0
    );
188
0
}
189
190
std::optional<StackOffset> State::canSwapWithNonTopArg(StackOffset const _offset) const
191
0
{
192
0
  auto const& slot = m_stackData[_offset.value];
193
0
  for (auto argOffset: stackArgsRange())
194
0
  {
195
0
    if (argOffset.value == m_stackData.size() - 1)
196
0
      continue;
197
0
    auto const& argSlot = m_stackData[argOffset.value];
198
0
    if (slot == argSlot)
199
0
      continue;
200
0
    bool const goodCandidateForSwap = !isArgsCompatible(argOffset, argOffset) && !requiredInArgs(argSlot);
201
0
    if (goodCandidateForSwap)
202
0
      return argOffset;
203
0
  }
204
0
  return std::nullopt;
205
0
}
206
207
Target const& State::target() const
208
0
{
209
0
  return m_target;
210
0
}
211
212
bool State::willRequireShrinking() const
213
0
{
214
0
  std::size_t deficit = 0u;
215
0
  for (auto const& [slot, minCount]: target().minCount)
216
0
  {
217
0
    if (slot.isJunk())
218
0
      continue;
219
0
    auto const currentCount = count(slot);
220
0
    if (currentCount < minCount)
221
0
      deficit += minCount - currentCount;
222
0
  }
223
0
  return deficit + size() > target().size;
224
0
}
225
226
std::optional<StackDepth> State::findDeepestIncorrectArgSlot() const
227
0
{
228
0
  for (StackOffset const offset: stackArgsRange())
229
0
  {
230
0
    if (!isArgsCompatible(offset, offset))
231
0
      return StackDepth{m_stackData.size() - offset.value};
232
0
  }
233
0
  return std::nullopt;
234
0
}