Coverage Report

Created: 2026-06-30 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/solidity/libyul/backends/evm/ssa/InstructionStore.h
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
 * Owns the instruction table of an SSACFG: hands out InstIds, stores opcode
20
 * payloads, and provides lookups. Has no knowledge of basic blocks or debug
21
 * information; SSACFG composes those concerns on top.
22
 *
23
 * Each Inst owns its payload directly via `std::unique_ptr<Payload>`. Tombstoning
24
 * an Inst (or flipping it via `replaceWith*`) resets the unique_ptr, releasing
25
 * the variant and any heap memory it transitively held. Slot storage and the free-list bookkeeping
26
 * live together in `m_insts`. Literal payloads are deduplicated through `m_literalDedup`.
27
 */
28
29
#pragma once
30
31
#include <libyul/backends/evm/ssa/util/TLSFFreeList.h>
32
#include <libyul/backends/evm/ssa/SSACFGTypes.h>
33
34
#include <libyul/AST.h>
35
#include <libyul/Builtins.h>
36
#include <libyul/Exceptions.h>
37
38
#include <libsolutil/Numeric.h>
39
40
#include <cstdint>
41
#include <limits>
42
#include <map>
43
#include <memory>
44
#include <utility>
45
#include <variant>
46
#include <vector>
47
48
namespace solidity::yul::ssa
49
{
50
51
class InstructionStore
52
{
53
public:
54
  using NumReturnsSizeType = std::uint8_t;
55
56
  struct LiteralPayload { u256 value; };
57
  struct UpsilonPayload { InstId targetPhi; };
58
  struct BuiltinCall
59
  {
60
    BuiltinHandle builtin;
61
    /// Literal-kind arguments
62
    std::vector<Literal> literalArguments;
63
  };
64
  struct Call
65
  {
66
    FunctionGraphID graphID;
67
    bool canContinue;
68
    std::size_t numReturns;
69
  };
70
71
  using Payload = std::variant<
72
    LiteralPayload,
73
    UpsilonPayload,
74
    BuiltinCall,
75
    Call
76
  >;
77
78
  struct Inst
79
  {
80
    InstOpcode opcode;
81
    BlockId block{};
82
    std::vector<InstId> inputs{};
83
    std::unique_ptr<Payload> payload{};
84
85
0
    constexpr bool isPhi() const noexcept { return opcode == InstOpcode::Phi; }
86
0
    constexpr bool isUpsilon() const noexcept { return opcode == InstOpcode::Upsilon; }
87
0
    constexpr bool isLiteral() const noexcept { return opcode == InstOpcode::Const; }
88
0
    constexpr bool isUnreachable() const noexcept { return opcode == InstOpcode::Unreachable; }
89
0
    constexpr bool isFunctionArg() const noexcept { return opcode == InstOpcode::FunctionArg; }
90
0
    constexpr bool isProjection() const noexcept { return opcode == InstOpcode::Projection; }
91
0
    constexpr bool isIdentity() const noexcept { return opcode == InstOpcode::Identity; }
92
0
    constexpr bool isNop() const noexcept { return opcode == InstOpcode::Nop; }
93
0
    constexpr bool isMemoryGuard() const noexcept { return opcode == InstOpcode::MemoryGuard; }
94
0
    constexpr bool isTombstone() const noexcept { return opcode == InstOpcode::Tombstone; }
95
    /// Operation = a Call, BuiltinCall, or MemoryGuard
96
    constexpr bool isOperation() const noexcept
97
0
    {
98
0
      return
99
0
        opcode == InstOpcode::Call ||
100
0
        opcode == InstOpcode::BuiltinCall ||
101
0
        opcode == InstOpcode::MemoryGuard;
102
0
    }
103
  };
104
105
0
  InstructionStore() = default;
106
  InstructionStore(InstructionStore const&) = delete;
107
  InstructionStore(InstructionStore&&) = default;
108
  InstructionStore& operator=(InstructionStore const&) = delete;
109
  InstructionStore& operator=(InstructionStore&&) = default;
110
0
  ~InstructionStore() = default;
111
112
0
  Inst& inst(InstId _id) { return m_insts[_id.value]; }
113
0
  Inst const& inst(InstId _id) const { return m_insts[_id.value]; }
114
0
  std::size_t numInsts() const { return m_insts.size(); }
115
0
  std::vector<Inst> const& instructions() const { return m_insts.data(); }
116
117
  /// Returns the opcode category for a given InstId.
118
0
  InstOpcode kindOf(InstId const _id) const { return inst(_id).opcode; }
119
120
  /// Returns the phi targeted by an Upsilon Inst.
121
0
  InstId upsilonPhi(InstId const _id) const { return payloadAs<InstOpcode::Upsilon, UpsilonPayload>(_id).targetPhi; }
122
123
  /// Returns the u256 payload of a Const Inst.
124
0
  u256 const& literalPayload(InstId const _id) const { return payloadAs<InstOpcode::Const, LiteralPayload>(_id).value; }
125
126
0
  BuiltinCall const& builtinPayload(InstId const _id) const { return payloadAs<InstOpcode::BuiltinCall, BuiltinCall>(_id); }
127
128
0
  Call const& callPayload(InstId const _id) const { return payloadAs<InstOpcode::Call, Call>(_id); }
129
130
  /// Returns the projection index of a Projection Inst
131
  NumReturnsSizeType projectionIndex(InstId const _id) const
132
0
  {
133
0
    Inst const& projectionInst = inst(_id);
134
0
    yulAssert(projectionInst.opcode == InstOpcode::Projection);
135
0
    yulAssert(projectionInst.inputs.size() == 1);
136
0
    InstId const producer = projectionInst.inputs.front();
137
0
    yulAssert(_id.value > producer.value);
138
0
    std::size_t const offset = static_cast<std::size_t>(_id.value) - static_cast<std::size_t>(producer.value) - 1;
139
0
    yulAssert(offset <= std::numeric_limits<NumReturnsSizeType>::max());
140
0
    return static_cast<NumReturnsSizeType>(offset);
141
0
  }
142
143
  /// Counts the trailing Projections immediately following `_producer` in `m_insts`
144
  NumReturnsSizeType numTrailingProjections(InstId const _producer) const
145
0
  {
146
0
    auto const& producerInst = inst(_producer);
147
0
    if (!producerInst.isOperation())
148
0
      return 0;
149
0
    std::size_t count = 0;
150
0
    while (true)
151
0
    {
152
0
      std::size_t const idx = static_cast<std::size_t>(_producer.value) + 1u + count;
153
0
      if (idx >= m_insts.size())
154
0
        break;
155
0
      using IndexType = util::TLSFFreeList<Inst, InstTombstone>::Index;
156
0
      yulAssert(idx < std::numeric_limits<IndexType>::max());
157
0
      auto const& trailing = m_insts[static_cast<IndexType>(idx)];
158
0
      if (!trailing.isProjection())
159
0
        break;
160
0
      yulAssert(trailing.inputs.size() == 1);
161
0
      yulAssert(trailing.inputs.front() == _producer);
162
0
      ++count;
163
0
      yulAssert(count <= std::numeric_limits<NumReturnsSizeType>::max());
164
0
    }
165
0
    return static_cast<NumReturnsSizeType>(count);
166
0
  }
167
168
  /// Allocates a new Phi Inst defined in `_definingBlock`. Returns the new InstId.
169
  InstId appendPhi(BlockId const _definingBlock)
170
0
  {
171
0
    return appendInst(Inst{InstOpcode::Phi, _definingBlock, {}, nullptr});
172
0
  }
173
174
  /// Allocates a new FunctionArg Inst defined in `_entryBlock`. Returns the new InstId.
175
  InstId appendFunctionArg(BlockId const _entryBlock)
176
0
  {
177
0
    return appendInst(Inst{InstOpcode::FunctionArg, _entryBlock, {}, nullptr});
178
0
  }
179
180
  /// Allocates a new MemoryGuard Inst defined in `_block`. The boundary value lives in `ControlFlowGraphs::memoryGuard`.
181
  InstId appendMemoryGuard(BlockId const _block)
182
0
  {
183
0
    yulAssert(_block.hasValue());
184
0
    return appendInst(Inst{InstOpcode::MemoryGuard, _block, {}, nullptr});
185
0
  }
186
187
  /// Allocates a new Unreachable Inst. Not pinned to any block (see class comment).
188
  InstId appendUnreachable()
189
0
  {
190
0
    return appendInst(Inst{InstOpcode::Unreachable, BlockId{}, {}, nullptr});
191
0
  }
192
193
  /// Allocates a new Const Inst with payload `_value`, pinned to `_entryBlock`.
194
  /// Literals are deduplicated by value (see class comment).
195
  /// Returns id of the corresponding Const instruction and a bool indicating
196
  /// whether new instruction has been appended (`true`) or existing instruction reused (`false`).
197
  std::pair<InstId, bool> appendLiteral(BlockId const _entryBlock, u256 _value)
198
0
  {
199
0
    yulAssert(_entryBlock.hasValue());
200
0
    if (auto const it = m_literalDedup.find(_value); it != m_literalDedup.end())
201
0
      return {it->second, false};
202
0
    InstId const id = appendInst(Inst{
203
0
      InstOpcode::Const,
204
0
      _entryBlock,
205
0
      {},
206
0
      std::make_unique<Payload>(LiteralPayload{_value})
207
0
    });
208
0
    m_literalDedup.emplace(std::move(_value), id);
209
0
    return {id, true};
210
0
  }
211
212
  /// Allocates a new single-output BuiltinCall Inst. Returns the new InstId.
213
  /// For multi-output BuiltinCalls, use `appendBuiltinCallWithProjections` so the
214
  /// producer + projections land in a contiguous cluster.
215
  InstId appendBuiltinCall(
216
    BlockId const _block,
217
    BuiltinCall _payload,
218
    std::vector<InstId> _inputs
219
  )
220
0
  {
221
0
    yulAssert(_block.hasValue());
222
0
    return appendInst(Inst{
223
0
      InstOpcode::BuiltinCall,
224
0
      _block,
225
0
      std::move(_inputs),
226
0
      std::make_unique<Payload>(std::move(_payload))
227
0
    });
228
0
  }
229
230
  /// Allocates a new single-output Call Inst. Returns the new InstId.
231
  /// For multi-output Calls, use `appendCallWithProjections` so the producer +
232
  /// projections land in a contiguous cluster.
233
  InstId appendCall(
234
    BlockId const _block,
235
    Call _payload,
236
    std::vector<InstId> _inputs
237
  )
238
0
  {
239
0
    yulAssert(_block.hasValue());
240
0
    yulAssert(_payload.numReturns < 2, "Multi-return Call must use appendCallWithProjections");
241
0
    return appendInst(Inst{
242
0
      InstOpcode::Call,
243
0
      _block,
244
0
      std::move(_inputs),
245
0
      std::make_unique<Payload>(std::move(_payload))
246
0
    });
247
0
  }
248
249
  /// Allocates a multi-return BuiltinCall along with its `_numReturns` trailing
250
  /// Projections in a contiguous cluster (length `1 + _numReturns`). Returns the
251
  /// producer's InstId; projections live at `producer.value + 1 .. + _numReturns`
252
  /// and the caller can read them via `SSACFG::projectionsOf`.
253
  InstId appendBuiltinCallWithProjections(
254
    BlockId const _block,
255
    BuiltinCall _payload,
256
    std::vector<InstId> _inputs,
257
    NumReturnsSizeType const _numReturns
258
  )
259
0
  {
260
0
    yulAssert(_block.hasValue());
261
0
    yulAssert(_numReturns >= 2, "appendBuiltinCallWithProjections requires _numReturns >= 2");
262
0
    std::uint32_t const length = static_cast<std::uint32_t>(_numReturns) + 1u;
263
0
    std::uint32_t const start = reserveRun(length);
264
0
    InstId const producerId{start};
265
0
    m_insts[start] = Inst{
266
0
      InstOpcode::BuiltinCall,
267
0
      _block,
268
0
      std::move(_inputs),
269
0
      std::make_unique<Payload>(std::move(_payload))
270
0
    };
271
0
    for (NumReturnsSizeType k = 0; k < _numReturns; ++k)
272
0
      m_insts[start + 1u + k] = Inst{InstOpcode::Projection, _block, {producerId}, nullptr};
273
0
    return producerId;
274
0
  }
275
276
  /// Allocates a multi-return Call along with its `_payload.numReturns` trailing
277
  /// Projections in a contiguous cluster. See `appendBuiltinCallWithProjections`.
278
  InstId appendCallWithProjections(
279
    BlockId const _block,
280
    Call _payload,
281
    std::vector<InstId> _inputs
282
  )
283
0
  {
284
0
    yulAssert(_block.hasValue());
285
0
    yulAssert(_payload.numReturns >= 2, "appendCallWithProjections requires _payload.numReturns >= 2");
286
0
    yulAssert(_payload.numReturns <= std::numeric_limits<NumReturnsSizeType>::max());
287
0
    auto const numReturns = static_cast<NumReturnsSizeType>(_payload.numReturns);
288
0
    std::uint32_t const length = static_cast<std::uint32_t>(numReturns) + 1u;
289
0
    std::uint32_t const start = reserveRun(length);
290
0
    InstId const producerId{start};
291
0
    m_insts[start] = Inst{
292
0
      InstOpcode::Call,
293
0
      _block,
294
0
      std::move(_inputs),
295
0
      std::make_unique<Payload>(std::move(_payload))
296
0
    };
297
0
    for (NumReturnsSizeType k = 0; k < numReturns; ++k)
298
0
      m_insts[start + 1u + k] = Inst{InstOpcode::Projection, _block, {producerId}, nullptr};
299
0
    return producerId;
300
0
  }
301
302
  /// Allocates a new Upsilon Inst targeting `_phi`, feeding `_value` from `_block`.
303
  InstId appendUpsilon(BlockId const _block, InstId const _value, InstId const _phi)
304
0
  {
305
0
    yulAssert(inst(_phi).isPhi());
306
0
    return appendInst(Inst{
307
0
      InstOpcode::Upsilon,
308
0
      _block,
309
0
      {_value},
310
0
      std::make_unique<Payload>(UpsilonPayload{_phi})
311
0
    });
312
0
  }
313
314
  /// Flips _target to Identity forwarding _forward.
315
  void replaceWithIdentity(InstId const _target, InstId const _forward)
316
0
  {
317
0
    yulAssert(_target.hasValue());
318
0
    yulAssert(_forward.hasValue());
319
0
    yulAssert(_target != _forward, "Cannot replace value with itself");
320
0
    auto& instruction = inst(_target);
321
    // every Const InstId is the unique handle for its value, so flipping it to Identity would silently rebind
322
    // every other user of that literal to _forward; almost certainly not what was intended
323
0
    yulAssert(!instruction.isLiteral(), "replaceWithIdentity must not morph a deduped Const slot");
324
0
    yulAssert(!instruction.isMemoryGuard(), "replaceWithIdentity must not morph a MemoryGuard slot");
325
0
    yulAssert(
326
0
      numTrailingProjections(_target) == 0,
327
0
      "replaceWithIdentity must not morph a multi-return producer slot"
328
0
    );
329
0
    instruction.opcode = InstOpcode::Identity;
330
0
    instruction.inputs.assign(1, _forward);
331
0
    instruction.payload.reset();
332
0
  }
333
334
  /// Flips _target to Const carrying _value. If _value is already deduped to another slot,
335
  /// flips _target to Identity forwarding to that slot instead.
336
  void replaceWithConst(InstId const _target, u256 const& _value)
337
0
  {
338
0
    yulAssert(_target.hasValue());
339
0
    auto& instruction = inst(_target);
340
0
    yulAssert(!instruction.isMemoryGuard(), "replaceWithConst must not morph a MemoryGuard slot");
341
0
    yulAssert(
342
0
      numTrailingProjections(_target) == 0,
343
0
      "replaceWithConst must not morph a multi-return producer slot"
344
0
    );
345
0
    // No-op if already this value.
346
0
    if (instruction.opcode == InstOpcode::Const && std::get<LiteralPayload>(*instruction.payload).value == _value)
347
0
      return;
348
0
    dropDedupIfConst(instruction);
349
0
    if (auto const it = m_literalDedup.find(_value); it != m_literalDedup.end())
350
0
    {
351
0
      instruction.opcode = InstOpcode::Identity;
352
0
      instruction.inputs.assign(1, it->second);
353
0
      instruction.payload.reset();
354
0
      return;
355
0
    }
356
0
    instruction.opcode = InstOpcode::Const;
357
0
    instruction.inputs.clear();
358
0
    instruction.payload = std::make_unique<Payload>(LiteralPayload{_value});
359
0
    m_literalDedup.emplace(std::move(_value), _target);
360
0
  }
361
362
  /// Flips _target to Nop. Caller must ensure _target produces no observable value (e.g., an Upsilon, or a void Call).
363
  void replaceWithNop(InstId const _target)
364
0
  {
365
0
    yulAssert(_target.hasValue());
366
0
    auto& instruction = inst(_target);
367
0
    yulAssert(!instruction.isMemoryGuard(), "replaceWithNop must not morph a MemoryGuard slot");
368
0
    yulAssert(
369
0
      numTrailingProjections(_target) == 0,
370
0
      "replaceWithNop must not morph a multi-return producer slot"
371
0
    );
372
0
    dropDedupIfConst(instruction);
373
0
    instruction.opcode = InstOpcode::Nop;
374
0
    instruction.inputs.clear();
375
0
    instruction.payload.reset();
376
0
  }
377
378
  /// Tombstones an Inst, releasing its slot(s) to the free pool. If `_id` is a
379
  /// multi-return producer (i.e. has trailing Projections), the entire cluster
380
  /// is swept and released as one contiguous run.
381
  void tombstone(InstId const _id)
382
0
  {
383
0
    yulAssert(_id.hasValue());
384
0
    auto const& instruction = inst(_id);
385
0
    if (instruction.isTombstone())
386
0
      return;
387
388
0
    yulAssert(
389
0
      !instruction.isProjection(),
390
0
      "illegal call of tombstone(_id) on a Projection, tombstone the producer instead"
391
0
    );
392
0
    std::uint32_t const trailingCount = numTrailingProjections(_id);
393
0
    dropDedupIfConst(instruction);
394
    // tombstone this inst and all trailing projections
395
0
    m_insts.deallocate(_id.value, trailingCount + 1u);
396
0
  }
397
398
private:
399
  /// Allocates a new Projection Inst projecting output of `_producer`.
400
  InstId appendProjection(BlockId const _block, InstId const _producer)
401
0
  {
402
0
    yulAssert(_block.hasValue());
403
0
    yulAssert(_producer.hasValue());
404
0
    return appendInst(Inst{InstOpcode::Projection, _block, {_producer}, nullptr});
405
0
  }
406
407
  /// Single-slot append: takes a length-1 run from the free pool if available, else
408
  /// tail-extends.
409
  InstId appendInst(Inst _inst)
410
0
  {
411
0
    yulAssert(m_insts.size() < std::numeric_limits<InstId::ValueType>::max());
412
0
    std::uint32_t const idx = m_insts.allocate(1);
413
0
    m_insts[idx] = std::move(_inst);
414
0
    return InstId{idx};
415
0
  }
416
417
  /// Reserves a contiguous run of `_length` slots in `m_insts`, returning the start
418
  /// index. The façade applies a smallest-fit policy with split-on-take and
419
  /// tail-extends with Tombstone placeholders on miss; reused cluster slots
420
  /// arrive already in tombstoned state.
421
  std::uint32_t reserveRun(std::uint32_t const _length)
422
0
  {
423
0
    yulAssert(_length >= 2, "reserveRun is for clusters; use appendInst for single slots");
424
0
    yulAssert(m_insts.size() < std::numeric_limits<InstId::ValueType>::max() - _length);
425
0
    return m_insts.allocate(_length);
426
0
  }
427
428
  /// Asserts that the Inst at `_id` has opcode `Expected`, then returns its payload as `T`.
429
  template<InstOpcode Expected, typename T>
430
  T const& payloadAs(InstId const _id) const
431
0
  {
432
0
    auto const& i = inst(_id);
433
0
    yulAssert(i.opcode == Expected);
434
0
    yulAssert(i.payload);
435
0
    return std::get<T>(*i.payload);
436
0
  }
Unexecuted instantiation: solidity::yul::ssa::InstructionStore::UpsilonPayload const& solidity::yul::ssa::InstructionStore::payloadAs<(solidity::yul::ssa::InstOpcode)2, solidity::yul::ssa::InstructionStore::UpsilonPayload>(solidity::yul::ssa::InstId) const
Unexecuted instantiation: solidity::yul::ssa::InstructionStore::LiteralPayload const& solidity::yul::ssa::InstructionStore::payloadAs<(solidity::yul::ssa::InstOpcode)0, solidity::yul::ssa::InstructionStore::LiteralPayload>(solidity::yul::ssa::InstId) const
Unexecuted instantiation: solidity::yul::ssa::InstructionStore::BuiltinCall const& solidity::yul::ssa::InstructionStore::payloadAs<(solidity::yul::ssa::InstOpcode)3, solidity::yul::ssa::InstructionStore::BuiltinCall>(solidity::yul::ssa::InstId) const
Unexecuted instantiation: solidity::yul::ssa::InstructionStore::Call const& solidity::yul::ssa::InstructionStore::payloadAs<(solidity::yul::ssa::InstOpcode)4, solidity::yul::ssa::InstructionStore::Call>(solidity::yul::ssa::InstId) const
437
438
  /// If `_inst` is a Const, removes its entry from the literal dedup table. Used by
439
  /// the replaceWith* primitives and tombstone() before overwriting the slot.
440
  void dropDedupIfConst(Inst const& _inst)
441
0
  {
442
0
    if (_inst.opcode != InstOpcode::Const)
443
0
      return;
444
0
    yulAssert(_inst.payload);
445
0
    u256 const& value = std::get<LiteralPayload>(*_inst.payload).value;
446
0
    m_literalDedup.erase(value);
447
0
  }
448
449
  struct InstTombstone
450
  {
451
    static Inst make() noexcept
452
0
    {
453
0
      return Inst{InstOpcode::Tombstone, BlockId{}, {}, nullptr};
454
0
    }
455
    static bool isTombstone(Inst const& _i) noexcept
456
0
    {
457
0
      return _i.opcode == InstOpcode::Tombstone;
458
0
    }
459
  };
460
461
  util::TLSFFreeList<Inst, InstTombstone> m_insts;
462
  std::map<u256, InstId> m_literalDedup;
463
};
464
465
}