• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SamboyCoding / Cpp2IL / 30769998286

02 Aug 2026 10:23PM UTC coverage: 34.169% (-1.8%) from 35.984%
30769998286

push

github

SamboyCoding
Decompiler: Support szarray, virtual calls, address of local, plus clean up ILSpy output by adjusting the IL

2899 of 9930 branches covered (29.19%)

Branch coverage included in aggregate %.

27 of 614 new or added lines in 19 files covered. (4.4%)

9 existing lines in 4 files now uncovered.

5316 of 14112 relevant lines covered (37.67%)

156656.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.92
/Cpp2IL.Core/Analysis/Simplifier.cs
1
using System.Collections.Generic;
2
using Cpp2IL.Core.Graphs;
3
using Cpp2IL.Core.ISIL;
4
using Cpp2IL.Core.Model.Contexts;
5

6
namespace Cpp2IL.Core.Analysis;
7

8
/// <summary>
9
/// Post-SSA copy/constant cleanup. The bulk of copy and constant propagation is done earlier, in SSA,
10
/// by <see cref="SsaSimplifier"/>; this pass mops up the copies that <em>destroying</em> SSA leaves
11
/// behind - each phi is lowered to a <c>Move</c> on every incoming edge, so the phi's result becomes a
12
/// single local with one definition per predecessor.
13
///
14
/// Those multiple definitions mean a value is no longer single-assignment: at the join the definitions
15
/// merge, and which one reaches a use is path-dependent. So unlike <see cref="SsaSimplifier"/>, this
16
/// pass cannot blindly forward a definition - it walks the CFG and refuses to carry a multiply-defined
17
/// local's value across the join its other definitions merge at (where the phi used to be).
18
/// </summary>
19
public static class Simplifier
20
{
21
    public static void Simplify(MethodAnalysisContext method)
22
    {
23
        var cfg = method.ControlFlowGraph!;
3✔
24

25
        InlineLocals(method);
3✔
26

27
        // Repeat until no change
28
        var changed = true;
3✔
29
        while (changed)
9✔
30
            changed = InlineConstantsSinglePass(cfg);
6✔
31

32
        // More locals can now be inlined
33
        InlineLocals(method);
3✔
34

35
        cfg.RemoveNops();
3✔
36
        cfg.RemoveEmptyBlocks();
3✔
37
    }
3✔
38

39
    private static bool InlineConstantsSinglePass(ISILControlFlowGraph graph)
40
    {
41
        var changed = false;
6✔
42
        var definitionCounts = CountDefinitions(graph);
6✔
43

44
        var visited = new HashSet<Block>();
6✔
45
        var queue = new Queue<Block>();
6✔
46

47
        queue.Enqueue(graph.EntryBlock);
6✔
48
        visited.Add(graph.EntryBlock);
6✔
49

50
        while (queue.Count > 0)
38✔
51
        {
52
            var block = queue.Dequeue();
32✔
53

54
            for (var i = 0; i < block.Instructions.Count; i++)
140✔
55
            {
56
                var instruction = block.Instructions[i];
38✔
57

58
                // If it's move and it moves something to local, replace and remove it
59
                if (instruction.OpCode == OpCode.Move && instruction.Operands[0] is LocalVariable local)
38!
60
                {
61
                    if (IsLocalUsedAfterInstruction(block, i + 1, local, out var usedByMemory))
9!
62
                    {
63
                        // This can't be inlined into memory operand
64
                        if (usedByMemory) continue;
9!
65

66
                        // A local with several definitions is not in SSA form, so its value at a join
67
                        // depends on the path taken; don't carry this definition across that join.
68
                        var stopAtJoins = definitionCounts.TryGetValue(local, out var defs) && defs > 1;
9!
69

70
                        // Replace local
71
                        ReplaceLocalsUntilReassignment(block, i + 1, local, instruction.Operands[1], stopAtJoins);
9✔
72

73
                        // Only drop the defining move once the local has no remaining uses; if the
74
                        // replacement stopped at a join, the local is still live past it so the move stays.
75
                        if (IsLocalUsedAfterInstruction(block, i + 1, local, out _))
9✔
76
                            continue;
77

78
                        // Change that move to nop
79
                        instruction.OpCode = OpCode.Nop;
5✔
80
                        instruction.SetOperands();
5✔
81

82
                        changed = true;
5✔
83
                    }
84
                }
85
            }
86

87
            foreach (var successor in block.Successors)
120✔
88
            {
89
                if (visited.Add(successor))
28✔
90
                    queue.Enqueue(successor);
26✔
91
            }
92
        }
93

94
        return changed;
6✔
95
    }
96

97
    private static void InlineLocals(MethodAnalysisContext method)
98
    {
99
        var graph = method.ControlFlowGraph;
6✔
100
        var definitionCounts = CountDefinitions(graph!);
6✔
101

102
        var visited = new HashSet<Block>();
6✔
103
        var queue = new Queue<Block>();
6✔
104

105
        queue.Enqueue(graph!.EntryBlock);
6✔
106
        visited.Add(graph.EntryBlock);
6✔
107

108
        while (queue.Count > 0)
38✔
109
        {
110
            var block = queue.Dequeue();
32✔
111

112
            for (var i = 0; i < block.Instructions.Count; i++)
140✔
113
            {
114
                var instruction = block.Instructions[i];
38✔
115

116
                // If it's move and it moves local to local, replace and remove it
117
                if (instruction.OpCode == OpCode.Move && instruction.Operands[0] is LocalVariable local && instruction.Operands[1] is LocalVariable source)
38!
118
                {
119
                    // A local with several definitions is not in SSA form, so its value at a join
120
                    // depends on the path taken; don't carry this definition across that join.
121
                    var stopAtJoins = definitionCounts.TryGetValue(local, out var defs) && defs > 1;
3!
122

123
                    // Replace local with source
124
                    ReplaceLocalsUntilReassignment(block, i + 1, local, source, stopAtJoins);
3✔
125

126
                    // If the replacement stopped at a join merging another definition, the local is
127
                    // still live there - keep its defining move rather than dropping the value on this path.
128
                    if (IsLocalUsedAfterInstruction(block, i + 1, local, out _))
3✔
129
                        continue;
130

131
                    if (!method.ParameterLocals.Contains(local))
1!
132
                        method.Locals.Remove(local);
1✔
133

134
                    // Change that move to nop
135
                    instruction.OpCode = OpCode.Nop;
1✔
136
                    instruction.SetOperands();
1✔
137
                }
138
            }
139

140
            foreach (var successor in block.Successors)
120✔
141
            {
142
                if (visited.Add(successor))
28✔
143
                    queue.Enqueue(successor);
26✔
144
            }
145
        }
146
    }
6✔
147

148
    // Counts how many instructions define each local. A local with more than one definition is not in
149
    // SSA form: at a control-flow join its value depends on which predecessor was taken, so none of its
150
    // definitions may be propagated across that join - a phi would be needed there instead.
151
    private static Dictionary<LocalVariable, int> CountDefinitions(ISILControlFlowGraph graph)
152
    {
153
        var counts = new Dictionary<LocalVariable, int>();
12✔
154

155
        foreach (var block in graph.Blocks)
152✔
156
        foreach (var instruction in block.Instructions)
280✔
157
            if (instruction.Destination is LocalVariable local)
76✔
158
                counts[local] = counts.TryGetValue(local, out var count) ? count + 1 : 1;
23✔
159

160
        return counts;
12✔
161
    }
162

163
    private static void ReplaceLocalsUntilReassignment(Block block, int startIndex, LocalVariable local, IOperand replacement, bool stopAtJoins)
164
    {
165
        var visited = new HashSet<(Block, int)>();
12✔
166

167
        void ProcessBlock(Block currentBlock, int index)
168
        {
169
            var key = (currentBlock, index);
27✔
170

171
            if (!visited.Add(key))
27!
172
                return;
×
173

174
            // Process instructions starting at the given index
175
            for (var i = index; i < currentBlock.Instructions.Count; i++)
104✔
176
            {
177
                var instruction = currentBlock.Instructions[i];
25✔
178

179
                // Stop on this branch when reassigned
180
                if (instruction.Destination is LocalVariable destLocal && destLocal == local)
25!
181
                    return;
×
182

183
                // Replace operands
184
                for (var j = 0; j < instruction.Operands.Count; j++)
124✔
185
                {
186
                    var operand = instruction.Operands[j];
37✔
187

188
                    if (operand is LocalVariable usedLocal && usedLocal == local)
37✔
189
                    {
190
                        instruction.SetOperand(j, replacement);
5✔
191
                    }
192

193
                    // A memory operand's base/index holds an address, so only a local replacement may
194
                    // be substituted there (copy propagation). A constant/value replacement is left in
195
                    // place - the caller sees the local is still used and keeps its defining move.
196
                    if (operand is MemoryOperand memory && replacement is LocalVariable)
37✔
197
                    {
198
                        if (memory.Base is LocalVariable baseLocal && baseLocal == local)
1!
199
                            memory.Base = replacement;
1✔
200

201
                        if (memory.Index is LocalVariable indexLocal && indexLocal == local)
1!
202
                            memory.Index = replacement;
×
203

204
                        instruction.SetOperand(j, memory);
1✔
205
                    }
206

207
                    // The object a field is accessed on is an address just like a memory base.
208
                    if (operand is FieldReference field && replacement is LocalVariable fieldReplacement && field.Local == local)
37!
209
                        field.Local = fieldReplacement;
×
210
                }
211
            }
212

213
            // Process successors
214
            foreach (var successor in currentBlock.Successors)
96✔
215
            {
216
                // A join merges this local's other definitions, so for a non-SSA (multiply-defined)
217
                // local the replacement must not flow past it - the value there is path-dependent.
218
                if (stopAtJoins && successor.Predecessors.Count > 1)
21✔
219
                    continue;
220

221
                ProcessBlock(successor, 0);
15✔
222
            }
223
        }
27✔
224

225
        ProcessBlock(block, startIndex);
12✔
226
    }
12✔
227

228
    private static bool IsLocalUsedAfterInstruction(Block block, int startIndex, LocalVariable local, out bool usedByMemory)
229
    {
230
        var visited = new HashSet<(Block, int)>();
21✔
231

232
        bool ProcessBlock(Block currentBlock, int index, out bool usedByMemory2)
233
        {
234
            usedByMemory2 = false;
46✔
235

236
            var key = (currentBlock, index);
46✔
237

238
            if (!visited.Add(key))
46!
239
                return false;
×
240

241
            // Process instructions
242
            for (var i = index; i < currentBlock.Instructions.Count; i++)
150✔
243
            {
244
                var instruction = currentBlock.Instructions[i];
44✔
245

246
                // Direct usage check
247
                if (instruction.Sources.Contains(local))
44✔
248
                    return true;
15✔
249

250
                // A field access reads the object it is on, whether the field is being read or written,
251
                // so the destination has to be considered too - a store is not in Sources.
252
                foreach (var operand in instruction.Operands)
140✔
253
                {
254
                    if (operand is FieldReference field && field.Local == local)
41!
255
                    {
256
                        usedByMemory2 = true;
×
257
                        return true;
×
258
                    }
259

260
                    // Likewise, an array element or length reads the array, and taking a slot's address reads it
261
                    // however the callee uses it - none of which are in Sources when they sit in a destination position.
262
                    if (operand is ArrayAccess array && (array.Array == local || array.Index == local as IOperand))
41!
263
                    {
NEW
264
                        usedByMemory2 = true;
×
NEW
265
                        return true;
×
266
                    }
267

268
                    if (operand is ArrayLength length && length.Array == local)
41!
269
                    {
NEW
270
                        usedByMemory2 = true;
×
NEW
271
                        return true;
×
272
                    }
273

274
                    if (operand is AddressOf { Target: LocalVariable addressed } && addressed == local)
41!
275
                    {
NEW
276
                        usedByMemory2 = true;
×
NEW
277
                        return true;
×
278
                    }
279
                }
280

281
                // Used in memory operand
282
                foreach (var source in instruction.Sources)
70✔
283
                {
284
                    if (source is MemoryOperand memory)
6✔
285
                    {
286
                        if (memory.Base is LocalVariable memLocal && memLocal == local)
2!
287
                        {
288
                            usedByMemory2 = true;
×
289
                            return true;
×
290
                        }
291

292
                        if (memory.Index is LocalVariable memLocal2 && memLocal2 == local)
2!
293
                        {
294
                            usedByMemory2 = true;
×
295
                            return true;
×
296
                        }
297
                    }
298
                }
299
            }
300

301
            // Process successors
302
            foreach (var successor in currentBlock.Successors)
102✔
303
            {
304
                if (ProcessBlock(successor, 0, out usedByMemory2))
25✔
305
                    return true;
10✔
306
            }
307

308
            return false;
21✔
309
        }
10✔
310

311
        return ProcessBlock(block, startIndex, out usedByMemory);
21✔
312
    }
313
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc