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/spill/SpillSet.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/spill/SpillSet.h>
20
21
#include <libyul/backends/evm/ssa/Stack.h>
22
#include <libyul/backends/evm/ssa/StackShuffler.h>
23
#include <libyul/backends/evm/ssa/StackLayout.h>
24
25
#include <deque>
26
27
using namespace solidity::yul::ssa;
28
using namespace solidity::yul::ssa::spill;
29
30
namespace
31
{
32
33
/// Build the symbolic stack right after `_value`'s operation completes
34
StackData computeOperationOut(
35
  SSACFG const& _cfg,
36
  SSACFGStackLayout const& _layout,
37
  InstId const _value
38
)
39
0
{
40
0
  InstId const producer = _cfg.isProjection(_value) ? _cfg.inst(_value).inputs.front() : _value;
41
42
0
  SSACFG::BlockId const block = _cfg.inst(producer).block;
43
0
  auto const& blockLayout = _layout[block];
44
0
  yulAssert(blockLayout, fmt::format("producer {}'s block has no layout", producer));
45
46
0
  std::size_t opIndex = 0;
47
0
  bool found = false;
48
0
  for (InstId const id: _cfg.block(block).instructions)
49
0
  {
50
0
    if (!_cfg.isOperation(id))
51
0
      continue;
52
0
    if (id == producer)
53
0
    {
54
0
      found = true;
55
0
      break;
56
0
    }
57
0
    ++opIndex;
58
0
  }
59
0
  yulAssert(found, fmt::format("producer {} not found in its block's instructions", producer));
60
0
  yulAssert(opIndex < blockLayout->operationIn.size());
61
62
0
  StackData opOutStack = blockLayout->operationIn[opIndex];
63
0
  SSACFG::Inst const& inst = _cfg.inst(producer);
64
  // a call that can continue also consumes its return label, which sits right below the inputs
65
0
  std::size_t consumedSlots = inst.inputs.size();
66
0
  if (inst.opcode == InstOpcode::Call && _cfg.callPayload(producer).canContinue)
67
0
    ++consumedSlots;
68
0
  yulAssert(opOutStack.size() >= consumedSlots, "operationIn smaller than consumed slot count");
69
0
  for (std::size_t i = 0; i < consumedSlots; ++i)
70
0
    opOutStack.pop_back();
71
0
  _cfg.forEachOutput(producer, [&](InstId const id) {
72
0
    opOutStack.push_back(StackSlot::makeValue(_cfg, id));
73
0
  });
74
0
  return opOutStack;
75
0
}
76
77
/// The symbolic stack the Emitter faces at `_value`'s definition, where its `mstore` fires. Three cases:
78
/// - a phi: the merged value is materialized on its defining block's `stackIn`, so a single store there covers every incoming edge;
79
/// - a function argument: it has no producer operation and lives on the function entry stack, where CodeTransform emits `mstore` while the args are still laid out;
80
/// - any other value: it sits on its producer's `operationOut`.
81
StackData defStackFor(
82
  SSACFG const& _cfg,
83
  SSACFGStackLayout const& _layout,
84
  InstId const _value
85
)
86
0
{
87
0
  if (_cfg.isPhi(_value))
88
0
  {
89
0
    SSACFG::BlockId const block = _cfg.inst(_value).block;
90
0
    yulAssert(block.hasValue(), fmt::format("phi {} has no defining block", _value));
91
0
    auto const& blockLayout = _layout[block];
92
0
    yulAssert(blockLayout, fmt::format("phi {}'s defining block has no layout", _value));
93
0
    return blockLayout->stackIn;
94
0
  }
95
0
  if (_cfg.isFunctionArg(_value))
96
0
  {
97
0
    auto const& entryLayout = _layout[_cfg.entry];
98
0
    yulAssert(entryLayout, "entry block has no layout for function-arg def-site");
99
0
    return entryLayout->stackIn;
100
0
  }
101
0
  return computeOperationOut(_cfg, _layout, _value);
102
0
}
103
104
}
105
106
void SpillSet::closeUnderReachabilityConstraints(SSACFG const& _cfg, SSACFGStackLayout const& _layout)
107
0
{
108
  // work queue over values that are marked for spillage
109
0
  std::deque<InstId> queue;
110
0
  for (InstId const id: spilledValues())
111
0
    queue.push_back(id);
112
113
0
  while (!queue.empty())
114
0
  {
115
0
    InstId const value = queue.front();
116
0
    queue.pop_front();
117
118
0
    StackData const defStack = defStackFor(_cfg, _layout, value);
119
0
    ensureDefSiteFeasible(_cfg, value, defStack, queue);
120
0
  }
121
0
}
122
123
void SpillSet::ensureDefSiteFeasible(
124
  SSACFG const& _cfg,
125
  InstId const _value,
126
  StackData const& _defStack,
127
  std::deque<InstId>& _workQueue)
128
0
{
129
  // predicate = spill set minus the owner; the shuffle accumulates discovered culprits here.
130
0
  SpillSet spillSetWithoutOwner = without(_value);
131
  // [... defStack ..., valueSlot]
132
0
  StackData const target = [&]{
133
0
    StackData result;
134
0
    result.reserve(_defStack.size() + 1);
135
0
    result.insert(result.end(), _defStack.begin(), _defStack.end());
136
0
    result.push_back(StackSlot::makeValue(_cfg, _value));
137
0
    return result;
138
0
  }();
139
0
  StackData workStack = _defStack;
140
0
  StackShufflerResult const result = shuffleWithSpillDiscovery(workStack, target, spillSetWithoutOwner);
141
0
  yulAssert(
142
0
    result.status == StackShufflerResult::Status::Admissible,
143
0
    fmt::format("def-site store for {} infeasible even after spilling siblings (status={})", _value, static_cast<int>(result.status))
144
0
  );
145
146
  // - if `_value` is reachable, it can be just DUPed and there shouldn't have been a stack too deep with it
147
  // - if `_value` is unreachable, there are > reachable stack depth distinct slots strictly above it and the
148
  //   shuffler heuristics should not pick anything that is already too deep as culprit
149
0
  yulAssert(!spillSetWithoutOwner.isSpilled(_value), "spill-aware shuffle reported the owner as its own blocker");
150
151
0
  for (InstId const culprit: spillSetWithoutOwner.spilledValues())
152
0
  {
153
0
    if (isSpilled(culprit))
154
0
      continue;
155
0
    add(culprit);
156
0
    _workQueue.push_back(culprit);
157
0
  }
158
0
}
159
160
SpillSet SpillSet::without(InstId const _id) const
161
0
{
162
0
  SpillSet result = *this;
163
0
  result.m_values.erase(_id);
164
0
  return result;
165
0
}