mirror of
https://github.com/AssetRipper/Cpp2IL.git
synced 2026-11-13 20:05:45 -10:00
374 lines
15 KiB
C#
374 lines
15 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Cpp2IL.Core.Graphs;
|
|
using Cpp2IL.Core.ISIL;
|
|
using Cpp2IL.Core.Model.Contexts;
|
|
|
|
namespace Cpp2IL.Core.Analysis;
|
|
|
|
/// <summary>
|
|
/// Post-SSA copy/constant cleanup. The bulk of copy and constant propagation is done earlier, in SSA,
|
|
/// by <see cref="SsaSimplifier"/>; this pass mops up the copies that <em>destroying</em> SSA leaves
|
|
/// behind - each phi is lowered to a <c>Move</c> on every incoming edge, so the phi's result becomes a
|
|
/// single local with one definition per predecessor.
|
|
///
|
|
/// Those multiple definitions mean a value is no longer single-assignment: at the join the definitions
|
|
/// merge, and which one reaches a use is path-dependent. So unlike <see cref="SsaSimplifier"/>, this
|
|
/// pass cannot blindly forward a definition - it walks the CFG and refuses to carry a multiply-defined
|
|
/// local's value across the join its other definitions merge at (where the phi used to be).
|
|
/// </summary>
|
|
public static class Simplifier
|
|
{
|
|
public static void Simplify(MethodAnalysisContext method)
|
|
{
|
|
new SimplifierContext(method).Process();
|
|
}
|
|
|
|
private readonly ref struct SimplifierContext(MethodAnalysisContext method)
|
|
{
|
|
private readonly Dictionary<Block, Dictionary<Instruction, OperandList>> _sourceCache = [];
|
|
private readonly MethodAnalysisContext _method = method;
|
|
private readonly ISILControlFlowGraph _graph = method.ControlFlowGraph!;
|
|
|
|
public void Process()
|
|
{
|
|
PopulateSourceCache();
|
|
|
|
InlineLocals();
|
|
|
|
// Repeat until no change
|
|
while (InlineConstantsSinglePass()) ;
|
|
|
|
// More locals can now be inlined
|
|
InlineLocals();
|
|
|
|
_graph.RemoveNops();
|
|
_graph.RemoveEmptyBlocks();
|
|
}
|
|
|
|
private void PopulateSourceCache()
|
|
{
|
|
#if NET5_0_OR_GREATER
|
|
_sourceCache.EnsureCapacity(_graph.Blocks.Count);
|
|
#endif
|
|
|
|
foreach (var block in _graph.Blocks)
|
|
{
|
|
var sourceCache = new Dictionary<Instruction, OperandList>(block.Instructions.Count);
|
|
foreach (var instruction in block.Instructions)
|
|
{
|
|
sourceCache[instruction] = instruction.Sources;
|
|
}
|
|
|
|
_sourceCache[block] = sourceCache;
|
|
}
|
|
}
|
|
|
|
private void UpdateSourceCache(Block block, Instruction instruction)
|
|
{
|
|
_sourceCache[block][instruction] = instruction.Operands;
|
|
}
|
|
|
|
private bool InlineConstantsSinglePass()
|
|
{
|
|
var changed = false;
|
|
var definitionCounts = CountDefinitions();
|
|
|
|
var visited = new HashSet<Block>();
|
|
var queue = new Queue<Block>(_graph.Blocks.Count);
|
|
|
|
#if NET5_0_OR_GREATER
|
|
visited.EnsureCapacity(_graph.Blocks.Count);
|
|
#endif
|
|
|
|
queue.Enqueue(_graph.EntryBlock);
|
|
visited.Add(_graph.EntryBlock);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var block = queue.Dequeue();
|
|
|
|
for (var i = 0; i < block.Instructions.Count; i++)
|
|
{
|
|
var instruction = block.Instructions[i];
|
|
|
|
// If it's move and it moves something to local, replace and remove it
|
|
if (instruction.OpCode == OpCode.Move && instruction.Operands[0] is LocalVariable local)
|
|
{
|
|
if (IsLocalUsedAfterInstruction(block, i + 1, local, out var usedByMemory))
|
|
{
|
|
// This can't be inlined into memory operand
|
|
if (usedByMemory) continue;
|
|
|
|
// A local with several definitions is not in SSA form, so its value at a join
|
|
// depends on the path taken; don't carry this definition across that join.
|
|
var stopAtJoins = definitionCounts.TryGetValue(local, out var defs) && defs > 1;
|
|
|
|
// Replace local
|
|
ReplaceLocalsUntilReassignment(block, i + 1, local, instruction.Operands[1], stopAtJoins);
|
|
|
|
// Only drop the defining move once the local has no remaining uses; if the
|
|
// replacement stopped at a join, the local is still live past it so the move stays.
|
|
if (IsLocalUsedAfterInstruction(block, i + 1, local, out _))
|
|
continue;
|
|
|
|
// Change that move to nop
|
|
instruction.OpCode = OpCode.Nop;
|
|
instruction.SetOperands();
|
|
UpdateSourceCache(block, instruction);
|
|
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var successor in block.Successors)
|
|
{
|
|
if (visited.Add(successor))
|
|
queue.Enqueue(successor);
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
private void InlineLocals()
|
|
{
|
|
var definitionCounts = CountDefinitions();
|
|
|
|
var visited = new HashSet<Block>();
|
|
var queue = new Queue<Block>(_method.ControlFlowGraph!.Blocks.Count);
|
|
|
|
#if NET5_0_OR_GREATER
|
|
visited.EnsureCapacity(_graph.Blocks.Count);
|
|
#endif
|
|
|
|
queue.Enqueue(_graph.EntryBlock);
|
|
visited.Add(_graph.EntryBlock);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var block = queue.Dequeue();
|
|
|
|
for (var i = 0; i < block.Instructions.Count; i++)
|
|
{
|
|
var instruction = block.Instructions[i];
|
|
|
|
// If it's move and it moves local to local, replace and remove it
|
|
if (instruction is { OpCode: OpCode.Move, Operands: [LocalVariable local, LocalVariable source] })
|
|
{
|
|
// A local with several definitions is not in SSA form, so its value at a join
|
|
// depends on the path taken; don't carry this definition across that join.
|
|
var stopAtJoins = definitionCounts.TryGetValue(local, out var defs) && defs > 1;
|
|
|
|
// Replace local with source
|
|
ReplaceLocalsUntilReassignment(block, i + 1, local, source, stopAtJoins);
|
|
|
|
// If the replacement stopped at a join merging another definition, the local is
|
|
// still live there - keep its defining move rather than dropping the value on this path.
|
|
if (IsLocalUsedAfterInstruction(block, i + 1, local, out _))
|
|
continue;
|
|
|
|
if (!_method.ParameterLocals.Contains(local))
|
|
_method.Locals.Remove(local);
|
|
|
|
// Change that move to nop
|
|
instruction.OpCode = OpCode.Nop;
|
|
instruction.SetOperands();
|
|
UpdateSourceCache(block, instruction);
|
|
}
|
|
}
|
|
|
|
foreach (var successor in block.Successors)
|
|
{
|
|
if (visited.Add(successor))
|
|
queue.Enqueue(successor);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Counts how many instructions define each local. A local with more than one definition is not in
|
|
// SSA form: at a control-flow join its value depends on which predecessor was taken, so none of its
|
|
// definitions may be propagated across that join - a phi would be needed there instead.
|
|
private Dictionary<LocalVariable, int> CountDefinitions()
|
|
{
|
|
var counts = new Dictionary<LocalVariable, int>();
|
|
|
|
foreach (var instruction in _graph.Blocks.SelectMany(block => block.Instructions))
|
|
{
|
|
if (instruction.Destination is LocalVariable local)
|
|
counts[local] = counts.TryGetValue(local, out var count) ? count + 1 : 1;
|
|
}
|
|
|
|
return counts;
|
|
}
|
|
|
|
private void ReplaceLocalsUntilReassignment(Block startBlock, int startIndex, LocalVariable local,
|
|
IOperand replacement, bool stopAtJoins)
|
|
{
|
|
var visited = new HashSet<Block>();
|
|
var remaining = new Stack<(Block, int)>(_graph.Blocks.Count);
|
|
|
|
#if NET5_0_OR_GREATER
|
|
visited.EnsureCapacity(_graph.Blocks.Count);
|
|
#endif
|
|
|
|
visited.Add(startBlock);
|
|
remaining.Push((startBlock, startIndex));
|
|
|
|
while (remaining.Count > 0)
|
|
{
|
|
var (currentBlock, index) = remaining.Pop();
|
|
|
|
// Process instructions starting at the given index
|
|
for (var i = index; i < currentBlock.Instructions.Count; i++)
|
|
{
|
|
var instruction = currentBlock.Instructions[i];
|
|
|
|
// Stop on this branch when reassigned
|
|
if (instruction.Destination is LocalVariable destLocal && destLocal == local)
|
|
return;
|
|
|
|
// Replace operands
|
|
for (var j = 0; j < instruction.Operands.Count; j++)
|
|
{
|
|
var operand = instruction.Operands[j];
|
|
|
|
if (operand is LocalVariable usedLocal && usedLocal == local)
|
|
{
|
|
instruction.SetOperand(j, replacement);
|
|
UpdateSourceCache(currentBlock, instruction);
|
|
}
|
|
|
|
// A memory operand's base/index holds an address, so only a local replacement may
|
|
// be substituted there (copy propagation). A constant/value replacement is left in
|
|
// place - the caller sees the local is still used and keeps its defining move.
|
|
else if (operand is MemoryOperand memory && replacement is LocalVariable)
|
|
{
|
|
if (memory.Base is LocalVariable baseLocal && baseLocal == local)
|
|
memory.Base = replacement;
|
|
|
|
if (memory.Index is LocalVariable indexLocal && indexLocal == local)
|
|
memory.Index = replacement;
|
|
|
|
instruction.SetOperand(j, memory);
|
|
UpdateSourceCache(currentBlock, instruction);
|
|
}
|
|
|
|
// The object a field is accessed on is an address just like a memory base.
|
|
else if (operand is FieldReference field && replacement is LocalVariable fieldReplacement &&
|
|
field.Local == local)
|
|
{
|
|
field.Local = fieldReplacement;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process successors
|
|
foreach (var successor in currentBlock.Successors)
|
|
{
|
|
// A join merges this local's other definitions, so for a non-SSA (multiply-defined)
|
|
// local the replacement must not flow past it - the value there is path-dependent.
|
|
if (stopAtJoins && successor.Predecessors.Count > 1)
|
|
continue;
|
|
|
|
if (visited.Add(successor))
|
|
remaining.Push((successor, 0));
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsLocalUsedAfterInstruction(Block startBlock, int startIndex, LocalVariable local, out bool usedByMemory)
|
|
{
|
|
var visited = new HashSet<Block>();
|
|
var remaining = new Stack<(Block, int)>(_graph.Blocks.Count);
|
|
|
|
#if NET5_0_OR_GREATER
|
|
visited.EnsureCapacity(_graph.Blocks.Count);
|
|
#endif
|
|
|
|
visited.Add(startBlock);
|
|
remaining.Push((startBlock, startIndex));
|
|
|
|
usedByMemory = false;
|
|
|
|
while (remaining.Count > 0)
|
|
{
|
|
var (currentBlock, index) = remaining.Pop();
|
|
|
|
var blockSources = _sourceCache[currentBlock];
|
|
|
|
// Process instructions
|
|
for (var i = index; i < currentBlock.Instructions.Count; i++)
|
|
{
|
|
var instruction = currentBlock.Instructions[i];
|
|
var sources = blockSources[instruction];
|
|
|
|
// Direct usage check
|
|
if (sources.Contains(local))
|
|
return true;
|
|
|
|
// A field access reads the object it is on, whether the field is being read or written,
|
|
// so the destination has to be considered too - a store is not in Sources.
|
|
foreach (var operand in instruction.Operands)
|
|
{
|
|
if (operand is FieldReference field && field.Local == local)
|
|
{
|
|
usedByMemory = true;
|
|
return true;
|
|
}
|
|
|
|
// Likewise, an array element or length reads the array, and taking a slot's address reads it
|
|
// however the callee uses it - none of which are in Sources when they sit in a destination position.
|
|
if (operand is ArrayAccess array && (array.Array == local || array.Index == local as IOperand))
|
|
{
|
|
usedByMemory = true;
|
|
return true;
|
|
}
|
|
|
|
if (operand is ArrayLength length && length.Array == local)
|
|
{
|
|
usedByMemory = true;
|
|
return true;
|
|
}
|
|
|
|
if (operand is AddressOf { Target: LocalVariable addressed } && addressed == local)
|
|
{
|
|
usedByMemory = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Used in memory operand
|
|
foreach (var source in sources)
|
|
{
|
|
if (source is MemoryOperand memory)
|
|
{
|
|
if (memory.Base is LocalVariable memLocal && memLocal == local)
|
|
{
|
|
usedByMemory = true;
|
|
return true;
|
|
}
|
|
|
|
if (memory.Index is LocalVariable memLocal2 && memLocal2 == local)
|
|
{
|
|
usedByMemory = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process successors
|
|
foreach (var successor in currentBlock.Successors)
|
|
{
|
|
if (visited.Add(successor))
|
|
remaining.Push((successor, 0));
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|