|
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ILLink.Shared.TypeSystemProxy;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
namespace Mono.Linker.Steps
{
//
// Evaluates simple properties or methods for constant expressions and
// then uses this information to remove unreachable conditional blocks and
// inline collected constants.
//
public class UnreachableBlocksOptimizer
{
readonly LinkContext _context;
readonly Dictionary<MethodDefinition, MethodResult?> _cache_method_results = new (2048);
readonly Stack<MethodDefinition> _resursion_guard = new ();
MethodDefinition? IntPtrSize, UIntPtrSize;
public UnreachableBlocksOptimizer (LinkContext context)
{
_context = context;
}
/// <summary>
/// Processes the specified method and perform all branch removal optimizations on it.
/// When this returns it's guaranteed that the method has been optimized (if possible).
/// </summary>
/// <param name="method">The method to process</param>
public void ProcessMethod (MethodDefinition method)
{
if (!IsMethodSupported (method))
return;
if (_context.Annotations.GetAction (method.Module.Assembly) != AssemblyAction.Link)
return;
var reducer = new BodyReducer (method.Body, _context);
try {
//
// If no external dependency can be extracted into constant there won't be
// anything to optimize in the method
//
if (!reducer.ApplyTemporaryInlining (this))
return;
//
// This is the main step which evaluates if any expression can
// produce folded branches. When it finds them the unreachable
// branch is removed.
//
if (reducer.RewriteBody ())
_context.LogMessage ($"Reduced '{reducer.InstructionsReplaced}' instructions in conditional branches for [{method.DeclaringType.Module.Assembly.Name}] method '{method.GetDisplayName ()}'.");
//
// Note: The inliner cannot run before reducer rewrites body as it
// would require another recomputing offsets due to instructions replacement
// done by inliner
//
var inliner = new CallInliner (method.Body, this);
inliner.RewriteBody ();
} catch (Exception e) {
throw new InternalErrorException ($"Could not process the body of method '{method.GetDisplayName ()}'.", e);
}
}
static bool IsMethodSupported (MethodDefinition method)
{
if (!method.HasBody)
return false;
//
// Block methods which rewrite does not support
//
switch (method.ReturnType.MetadataType) {
case MetadataType.ByReference:
case MetadataType.FunctionPointer:
return false;
}
return true;
}
static bool HasJumpIntoTargetRange (Collection<Instruction> instructions, int firstInstr, int lastInstr, Func<Instruction, int?>? mapping = null)
{
foreach (var instr in instructions) {
switch (instr.OpCode.FlowControl) {
case FlowControl.Branch:
case FlowControl.Cond_Branch:
if (instr.Operand is Instruction target) {
if (mapping != null && mapping (target) is int index) {
if (index >= firstInstr && index <= lastInstr) {
return true;
}
}
else {
for (int i = firstInstr; i <= lastInstr; i++) {
if (instructions[i] == target) {
return true;
}
}
}
} else {
foreach (var rtarget in (Instruction[]) instr.Operand) {
if (mapping != null && mapping (rtarget) is int index) {
if (index >= firstInstr && index <= lastInstr) {
return true;
}
}
else {
for (int i = firstInstr; i <= lastInstr; i++) {
if (instructions[i] == rtarget) {
return true;
}
}
}
}
}
break;
}
}
return false;
}
static bool IsSideEffectFreeLoad (Instruction instr)
{
switch (instr.OpCode.Code) {
case Code.Ldarg:
case Code.Ldloc:
case Code.Ldloc_0:
case Code.Ldloc_1:
case Code.Ldloc_2:
case Code.Ldloc_3:
case Code.Ldloc_S:
case Code.Ldc_I4_0:
case Code.Ldc_I4_1:
case Code.Ldc_I4_2:
case Code.Ldc_I4_3:
case Code.Ldc_I4_4:
case Code.Ldc_I4_5:
case Code.Ldc_I4_6:
case Code.Ldc_I4_7:
case Code.Ldc_I4_8:
case Code.Ldc_I4:
case Code.Ldc_I4_S:
case Code.Ldc_I4_M1:
case Code.Ldc_I8:
case Code.Ldc_R4:
case Code.Ldc_R8:
case Code.Ldnull:
case Code.Ldstr:
return true;
}
return false;
}
static bool IsComparisonAlwaysTrue (OpCode opCode, int left, int right)
{
switch (opCode.Code) {
case Code.Beq:
case Code.Beq_S:
case Code.Ceq:
return left == right;
case Code.Bne_Un:
case Code.Bne_Un_S:
return left != right;
case Code.Bge:
case Code.Bge_S:
return left >= right;
case Code.Bge_Un:
case Code.Bge_Un_S:
return (uint) left >= (uint) right;
case Code.Bgt:
case Code.Bgt_S:
case Code.Cgt:
return left > right;
case Code.Bgt_Un:
case Code.Bgt_Un_S:
return (uint) left > (uint) right;
case Code.Ble:
case Code.Ble_S:
return left <= right;
case Code.Ble_Un:
case Code.Ble_Un_S:
return (uint) left <= (uint) right;
case Code.Blt:
case Code.Blt_S:
case Code.Clt:
return left < right;
case Code.Blt_Un:
case Code.Blt_Un_S:
return (uint) left < (uint) right;
}
throw new NotImplementedException (opCode.ToString ());
}
MethodResult? AnalyzeMethodForConstantResult (in CalleePayload callee, Stack<MethodDefinition> callStack)
{
MethodDefinition method = callee.Method;
if (method.ReturnType.MetadataType == MetadataType.Void)
return null;
if (!method.HasBody)
return null;
switch (_context.Annotations.GetAction (method)) {
case MethodAction.ConvertToThrow:
return null;
case MethodAction.ConvertToStub:
Instruction? constant = CodeRewriterStep.CreateConstantResultInstruction (_context, method);
return constant == null ? null : new MethodResult (constant, !HasSideEffects (method));
}
if (method.IsIntrinsic () || method.NoInlining)
return null;
if (!_context.IsOptimizationEnabled (CodeOptimizations.IPConstantPropagation, method))
return null;
var analyzer = new ConstantExpressionMethodAnalyzer (this);
if (analyzer.Analyze (callee, callStack))
return new MethodResult (analyzer.Result, analyzer.SideEffectFreeResult);
return null;
}
static bool HasSideEffects (MethodDefinition method)
{
return !method.DeclaringType.IsBeforeFieldInit;
}
//
// Return expression with a value when method implementation can be
// interpreted during trimming
//
MethodResult? TryGetMethodCallResult (in CalleePayload callee)
{
_resursion_guard.Clear ();
return TryGetMethodCallResult (callee, _resursion_guard);
}
MethodResult? TryGetMethodCallResult (in CalleePayload callee, Stack<MethodDefinition> callStack)
{
MethodResult? value;
MethodDefinition method = callee.Method;
if (!method.HasMetadataParameters () || callee.HasUnknownArguments) {
if (!_cache_method_results.TryGetValue (method, out value) && !IsDeepStack (callStack)) {
value = AnalyzeMethodForConstantResult (callee, callStack);
_cache_method_results.Add (method, value);
}
return value;
}
return AnalyzeMethodForConstantResult (callee, callStack);
static bool IsDeepStack (Stack<MethodDefinition> callStack) => callStack.Count > 100;
}
Instruction? GetSizeOfResult (TypeReference type)
{
MethodDefinition? sizeOfImpl = null;
//
// sizeof (IntPtr) and sizeof (UIntPtr) are just aliases for IntPtr.Size and UIntPtr.Size
// which are simple static properties commonly overwritten. Instead of forcing C# code style
// we handle both via static get_Size method
//
if (type.MetadataType == MetadataType.UIntPtr) {
sizeOfImpl = (UIntPtrSize ??= FindSizeMethod (_context.TryResolve (type)));
} else if (type.MetadataType == MetadataType.IntPtr) {
sizeOfImpl = (IntPtrSize ??= FindSizeMethod (_context.TryResolve (type)));
}
if (sizeOfImpl == null)
return null;
return TryGetMethodCallResult (new CalleePayload (sizeOfImpl, Array.Empty<Instruction> ()))?.Instruction;
}
static Instruction? EvaluateIntrinsicCall (MethodReference method, Instruction[] arguments)
{
//
// In theory any pure method could be executed via reflection but
// that would require loading all code path dependencies.
// For now we handle only few methods that help with core framework trimming
//
object? left, right;
if (method.DeclaringType.MetadataType == MetadataType.String) {
switch (method.Name) {
case "op_Equality":
case "op_Inequality":
case "Concat":
if (arguments.Length != 2)
return null;
if (!GetConstantValue (arguments[0], out left) ||
!GetConstantValue (arguments[1], out right))
return null;
if (left is string sleft && right is string sright) {
if (method.Name.Length == 6) // Concat case
return Instruction.Create (OpCodes.Ldstr, string.Concat (sleft, sright));
bool result = method.Name.Length == 11 ? sleft == sright : sleft != sright;
return Instruction.Create (OpCodes.Ldc_I4, result ? 1 : 0); // op_Equality / op_Inequality
}
break;
}
}
return null;
}
static Instruction[]? GetArgumentsOnStack (MethodDefinition method, Collection<Instruction> instructions, int index)
{
if (!method.HasMetadataParameters ())
return Array.Empty<Instruction> ();
Instruction[]? result = null;
for (int i = method.GetMetadataParametersCount (), pos = 0; i != 0; --i, ++pos) {
Instruction instr = instructions[index - i];
if (!IsConstantValue (instr))
return null;
result ??= new Instruction[method.GetMetadataParametersCount ()];
result[pos] = instr;
}
if (result != null && HasJumpIntoTargetRange (instructions, index - method.GetMetadataParametersCount () + 1, index))
return null;
return result;
static bool IsConstantValue (Instruction instr)
{
switch (instr.OpCode.Code) {
case Code.Ldc_I4_0:
case Code.Ldc_I4_1:
case Code.Ldc_I4_2:
case Code.Ldc_I4_3:
case Code.Ldc_I4_4:
case Code.Ldc_I4_5:
case Code.Ldc_I4_6:
case Code.Ldc_I4_7:
case Code.Ldc_I4_8:
case Code.Ldc_I4:
case Code.Ldc_I4_S:
case Code.Ldc_I4_M1:
case Code.Ldc_I8:
case Code.Ldc_R4:
case Code.Ldc_R8:
case Code.Ldnull:
case Code.Ldstr:
return true;
}
return false;
}
}
static bool GetConstantValue (Instruction instruction, out object? value)
{
switch (instruction.OpCode.Code) {
case Code.Ldc_I4_0:
value = 0;
return true;
case Code.Ldc_I4_1:
value = 1;
return true;
case Code.Ldc_I4_2:
value = 2;
return true;
case Code.Ldc_I4_3:
value = 3;
return true;
case Code.Ldc_I4_4:
value = 4;
return true;
case Code.Ldc_I4_5:
value = 5;
return true;
case Code.Ldc_I4_6:
value = 6;
return true;
case Code.Ldc_I4_7:
value = 7;
return true;
case Code.Ldc_I4_8:
value = 8;
return true;
case Code.Ldc_I4_M1:
value = -1;
return true;
case Code.Ldc_I4:
value = (int) instruction.Operand;
return true;
case Code.Ldc_I4_S:
value = (int) (sbyte) instruction.Operand;
return true;
case Code.Ldc_I8:
value = (long) instruction.Operand;
return true;
case Code.Ldstr:
value = (string) instruction.Operand;
return true;
case Code.Ldnull:
value = null;
return true;
default:
value = null;
return false;
}
}
static MethodDefinition? FindSizeMethod (TypeDefinition? type)
{
if (type == null)
return null;
return type.Methods.First (l => !l.HasMetadataParameters () && l.IsStatic && l.Name == "get_Size");
}
readonly struct CallInliner
{
readonly MethodBody body;
readonly UnreachableBlocksOptimizer optimizer;
public CallInliner (MethodBody body, UnreachableBlocksOptimizer optimizer)
{
this.body = body;
this.optimizer = optimizer;
}
public bool RewriteBody ()
{
bool changed = false;
LinkerILProcessor processor = body.GetLinkerILProcessor ();
#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
Collection<Instruction> instrs = body.Instructions;
#pragma warning restore RS0030
for (int i = 0; i < instrs.Count; ++i) {
Instruction instr = instrs[i];
switch (instr.OpCode.Code) {
case Code.Call:
case Code.Callvirt:
MethodDefinition? md = optimizer._context.TryResolve ((MethodReference) instr.Operand);
if (md == null)
continue;
if (md.IsVirtual)
continue;
if (md.CallingConvention == MethodCallingConvention.VarArg)
break;
if (md.NoInlining)
break;
var cpl = new CalleePayload (md, GetArgumentsOnStack (md, instrs, i));
MethodResult? call_result = optimizer.TryGetMethodCallResult (cpl);
if (call_result is not MethodResult result)
break;
if (!result.IsSideEffectFree) {
optimizer._context.LogMessage ($"Cannot inline constant result of '{md.GetDisplayName ()}' call due to presence of side effects");
break;
}
if (!md.IsStatic) {
if (!md.HasMetadataParameters () && CanInlineInstanceCall (instrs, i)) {
processor.Replace (i - 1, Instruction.Create (OpCodes.Nop));
processor.Replace (i, result.GetPrototype ()!);
changed = true;
}
continue;
}
if (md.HasMetadataParameters ()) {
if (!IsCalledWithoutSideEffects (md, instrs, i))
continue;
for (int p = 1; p <= md.GetMetadataParametersCount (); ++p) {
processor.Replace (i - p, Instruction.Create (OpCodes.Nop));
}
}
processor.Replace (i, result.GetPrototype ());
changed = true;
continue;
case Code.Sizeof:
var operand = (TypeReference) instr.Operand;
Instruction? value = optimizer.GetSizeOfResult (operand);
if (value != null) {
processor.Replace (i, value.GetPrototype ());
changed = true;
}
continue;
}
}
return changed;
}
bool CanInlineInstanceCall (Collection<Instruction> instructions, int index)
{
//
// Instance methods called on `this` have no side-effects
//
if (instructions[index - 1].OpCode.Code == Code.Ldarg_0)
return !body.Method.IsStatic;
// More cases can be added later
return false;
}
static bool IsCalledWithoutSideEffects (MethodDefinition method, Collection<Instruction> instructions, int index)
{
for (int i = 1; i <= method.GetMetadataParametersCount (); ++i) {
if (!IsSideEffectFreeLoad (instructions[index - i]))
return false;
}
return true;
}
}
struct BodyReducer
{
readonly LinkContext context;
Dictionary<Instruction, int>? mapping;
//
// Sorted list of body instruction indexes which were
// replaced pass-through nop
//
List<int>? conditionInstrsToRemove;
//
// Sorted list of body instruction indexes which were
// set to be replaced with different intstruction
//
List<(int, Instruction)>? conditionInstrsToReplace;
public BodyReducer (MethodBody body, LinkContext context)
{
Body = body;
this.context = context;
FoldedInstructions = null;
mapping = null;
conditionInstrsToRemove = null;
conditionInstrsToReplace = null;
InstructionsReplaced = 0;
}
public MethodBody Body { get; }
#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
Collection<Instruction> Instructions => Body.Instructions;
Collection<ExceptionHandler> ExceptionHandlers => Body.ExceptionHandlers;
#pragma warning restore RS0030
public int InstructionsReplaced { get; set; }
Collection<Instruction>? FoldedInstructions { get; set; }
[MemberNotNull (nameof(FoldedInstructions))]
[MemberNotNull (nameof(mapping))]
void InitializeFoldedInstruction ()
{
FoldedInstructions = new Collection<Instruction> (Instructions);
mapping = new Dictionary<Instruction, int> ();
}
public void Rewrite (int index, Instruction newInstruction)
{
if (FoldedInstructions == null)
InitializeFoldedInstruction ();
Debug.Assert (mapping != null);
// Tracks mapping for replaced instructions for easier
// branch targets resolution later
mapping[Instructions[index]] = index;
FoldedInstructions[index] = newInstruction;
}
void RewriteCondition (int index, Instruction instr, int operand)
{
switch (instr.OpCode.Code) {
case Code.Brfalse:
case Code.Brfalse_S:
if (operand == 0) {
Rewrite (index, Instruction.Create (OpCodes.Br, (Instruction) instr.Operand));
} else {
RewriteConditionToNop (index);
}
break;
case Code.Brtrue:
case Code.Brtrue_S:
if (operand != 0) {
Rewrite (index, Instruction.Create (OpCodes.Br, (Instruction) instr.Operand));
} else {
RewriteConditionToNop (index);
}
break;
case Code.Switch:
var targets = (Instruction[]) instr.Operand;
if (operand < targets.Length) {
// It does not need to be conditional but existing logic in BodySweeper would
// need to be updated to deal with 1->2 instruction replacement
RewriteConditionTo (index, Instruction.Create (operand == 0 ? OpCodes.Brfalse : OpCodes.Brtrue, targets[operand]));
Rewrite (index, Instruction.Create (OpCodes.Br, targets[operand]));
} else {
RewriteConditionToNop (index);
}
break;
}
}
void RewriteConditionToNop (int index)
{
conditionInstrsToRemove ??= new List<int> ();
conditionInstrsToRemove.Add (index);
RewriteToNop (index);
}
void RewriteConditionTo (int index, Instruction instruction)
{
conditionInstrsToReplace ??= new List<(int, Instruction)> ();
conditionInstrsToReplace.Add ((index, instruction));
}
public void RewriteToNop (int index, int stackDepth)
{
if (FoldedInstructions == null)
InitializeFoldedInstruction ();
int start_index;
for (start_index = index; start_index >= 0 && stackDepth > 0; --start_index) {
stackDepth -= GetStackBehaviourDelta (FoldedInstructions[start_index], out bool undefined);
if (undefined)
return;
}
if (stackDepth != 0) {
Debug.Fail ("Invalid IL?");
return;
}
while (start_index != index)
RewriteToNop (++start_index);
}
static int GetStackBehaviourDelta (Instruction instruction, out bool unknown)
{
int delta = 0;
unknown = false;
switch (instruction.OpCode.StackBehaviourPop) {
case StackBehaviour.Pop0:
break;
case StackBehaviour.Pop1:
case StackBehaviour.Popref:
case StackBehaviour.Popi:
--delta;
break;
case StackBehaviour.Pop1_pop1:
case StackBehaviour.Popi_pop1:
case StackBehaviour.Popi_popi:
case StackBehaviour.Popi_popi8:
case StackBehaviour.Popi_popr4:
case StackBehaviour.Popi_popr8:
case StackBehaviour.Popref_pop1:
case StackBehaviour.Popref_popi:
delta -= 2;
break;
case StackBehaviour.Popi_popi_popi:
case StackBehaviour.Popref_popi_popi:
case StackBehaviour.Popref_popi_popi8:
case StackBehaviour.Popref_popi_popr4:
case StackBehaviour.Popref_popi_popr8:
case StackBehaviour.Popref_popi_popref:
delta -= 3;
break;
case StackBehaviour.Varpop:
if (instruction.Operand is IMethodSignature ms) {
if (ms.HasThis && instruction.OpCode != OpCodes.Newobj)
--delta;
delta -= ms.Parameters.Count;
break;
}
if (instruction.OpCode == OpCodes.Ret) {
unknown = true;
return 0;
}
Debug.Fail (instruction.Operand?.ToString ());
unknown = true;
return 0;
default:
Debug.Fail (instruction.OpCode.StackBehaviourPop.ToString ());
unknown = true;
return 0;
}
switch (instruction.OpCode.StackBehaviourPush) {
case StackBehaviour.Push0:
break;
case StackBehaviour.Push1:
case StackBehaviour.Pushi:
case StackBehaviour.Pushi8:
case StackBehaviour.Pushr4:
case StackBehaviour.Pushr8:
case StackBehaviour.Pushref:
++delta;
break;
case StackBehaviour.Push1_push1:
delta += 2;
break;
case StackBehaviour.Varpush:
if (instruction.Operand is IMethodSignature ms) {
if (ms.ReturnType.MetadataType != MetadataType.Void)
++delta;
break;
}
Debug.Fail (instruction.Operand?.ToString ());
unknown = true;
return 0;
default:
Debug.Fail (instruction.OpCode.StackBehaviourPush.ToString ());
unknown = true;
return 0;
}
return delta;
}
void RewriteToNop (int index)
{
Rewrite (index, Instruction.Create (OpCodes.Nop));
}
public bool RewriteBody ()
{
if (FoldedInstructions == null)
InitializeFoldedInstruction ();
if (!RemoveConditions ())
return false;
BitArray reachableInstrs = GetReachableInstructionsMap (out var unreachableEH);
if (reachableInstrs == null)
return false;
var bodySweeper = new BodySweeper (Body, reachableInstrs, unreachableEH, context);
bodySweeper.Initialize ();
bodySweeper.Process (conditionInstrsToRemove, conditionInstrsToReplace, out var nopInstructions);
InstructionsReplaced = bodySweeper.InstructionsReplaced;
if (InstructionsReplaced == 0)
return false;
reachableInstrs = GetReachableInstructionsMap (out _);
if (reachableInstrs != null)
RemoveUnreachableInstructions (reachableInstrs);
if (nopInstructions != null) {
LinkerILProcessor processor = Body.GetLinkerILProcessor ();
foreach (var instr in nopInstructions)
processor.Remove (instr);
}
return true;
}
public bool ApplyTemporaryInlining (in UnreachableBlocksOptimizer optimizer)
{
bool changed = false;
var instructions = Instructions;
Instruction? targetResult;
for (int i = 0; i < instructions.Count; ++i) {
var instr = instructions[i];
switch (instr.OpCode.Code) {
case Code.Call:
case Code.Callvirt:
var md = context.TryResolve ((MethodReference) instr.Operand);
if (md == null)
break;
// Not supported
if (md.IsVirtual || md.CallingConvention == MethodCallingConvention.VarArg)
break;
Instruction[]? args = GetArgumentsOnStack (md, FoldedInstructions ?? instructions, i);
targetResult = args?.Length > 0 && md.IsStatic ? EvaluateIntrinsicCall (md, args) : null;
targetResult ??= optimizer.TryGetMethodCallResult (new CalleePayload (md, args))?.Instruction;
if (targetResult == null)
break;
//
// Do simple arguments stack removal by replacing argument expressions with nops. For cases
// that require full stack understanding the logic won't work and will leave more opcodes
// on the stack and constant won't be propagated
//
int depth = args?.Length ?? 0;
if (!md.IsStatic)
++depth;
if (depth != 0)
RewriteToNop (i - 1, depth);
Rewrite (i, targetResult);
changed = true;
break;
case Code.Ldsfld:
var ftarget = (FieldReference) instr.Operand;
var field = context.TryResolve (ftarget);
if (field == null)
break;
if (context.Annotations.TryGetFieldUserValue (field, out object? value)) {
targetResult = CodeRewriterStep.CreateConstantResultInstruction (context, field.FieldType, value);
if (targetResult == null)
break;
Rewrite (i, targetResult);
changed = true;
}
break;
case Code.Sizeof:
var operand = (TypeReference) instr.Operand;
targetResult = optimizer.GetSizeOfResult (operand);
if (targetResult != null) {
Rewrite (i, targetResult);
changed = true;
}
break;
}
}
return changed;
}
static bool IsConditionalBranch (OpCode opCode)
=> opCode.Code is Code.Brfalse or Code.Brfalse_S or Code.Brtrue or Code.Brtrue_S;
void RemoveUnreachableInstructions (BitArray reachable)
{
LinkerILProcessor processor = Body.GetLinkerILProcessor ();
int removed = 0;
for (int i = 0; i < reachable.Count; ++i) {
if (reachable[i])
continue;
int index = i - removed;
// If we intend to remove the last instruction we replaced it with "ret" above (not "nop")
// but we can't get rid of it completely because it may happen that the last kept instruction
// is a conditional branch - in which case to keep the IL valid, there has to be something after
// the conditional branch instruction (the else branch). So if that's the case
// inject "ldnull; throw;" at the end - this branch should never be reachable and it's always valid
// (ret may need to return a value of the right type if the method has a return value which is complicated
// to construct out of nothing).
if (index == Instructions.Count - 1 && Instructions[index].OpCode == OpCodes.Ret &&
index > 0 && IsConditionalBranch (Instructions[index - 1].OpCode)) {
processor.Replace (index, Instruction.Create (OpCodes.Ldnull));
processor.InsertAfter (Instructions[index], Instruction.Create (OpCodes.Throw));
} else {
processor.RemoveAt (index);
++removed;
}
}
}
bool RemoveConditions ()
{
Debug.Assert (FoldedInstructions != null);
bool changed = false;
object? left, right;
//
// Finds any branchable instruction and checks if the operand or operands
// can be evaluated as constant result.
//
// The logic does not remove any instructions but replaces them with nops for
// easier processing later (makes the mapping straigh-forward).
//
for (int i = 0; i < FoldedInstructions.Count; ++i) {
var instr = FoldedInstructions[i];
var opcode = instr.OpCode;
if (opcode.FlowControl == FlowControl.Cond_Branch) {
if (opcode.StackBehaviourPop == StackBehaviour.Pop1_pop1) {
if (!GetOperandsConstantValues (i, out left, out right))
continue;
if (left is int lint && right is int rint) {
if (IsJumpTargetRange (i - 1, i))
continue;
RewriteToNop (i - 2);
RewriteToNop (i - 1);
if (IsComparisonAlwaysTrue (opcode, lint, rint)) {
Rewrite (i, Instruction.Create (OpCodes.Br, (Instruction) instr.Operand));
} else {
RewriteConditionToNop (i);
}
changed = true;
continue;
}
continue;
}
if (opcode.StackBehaviourPop == StackBehaviour.Popi) {
if (i > 0 && GetConstantValue (FoldedInstructions[i - 1], out var operand)) {
if (operand is int opint) {
if (IsJumpTargetRange (i, i))
continue;
RewriteToNop (i - 1);
RewriteCondition (i, instr, opint);
changed = true;
continue;
}
if (operand is null && (opcode.Code == Code.Brfalse || opcode.Code == Code.Brfalse_S)) {
if (IsJumpTargetRange (i, i))
continue;
RewriteToNop (i - 1);
Rewrite (i, Instruction.Create (OpCodes.Br, (Instruction) instr.Operand));
changed = true;
continue;
}
}
// Common pattern generated by C# compiler in debug mode
if (i >= 3 && GetConstantValue (FoldedInstructions[i - 3], out operand) && operand is int opint2 && IsPairedStlocLdloc (FoldedInstructions[i - 2], FoldedInstructions[i - 1])) {
if (IsJumpTargetRange (i - 2, i))
continue;
RewriteToNop (i - 3);
RewriteToNop (i - 2);
RewriteToNop (i - 1);
RewriteCondition (i, instr, opint2);
changed = true;
continue;
}
// Pattern for non-zero based switch with constant input
if (i >= 5 && opcode == OpCodes.Switch && GetConstantValue (FoldedInstructions[i - 5], out operand) && operand is int opint3 && IsPairedStlocLdloc (FoldedInstructions[i - 4], FoldedInstructions[i - 3])) {
if (IsJumpTargetRange (i - 4, i))
continue;
if (!GetConstantValue (FoldedInstructions[i - 2], out operand) || operand is not int offset)
continue;
if (FoldedInstructions[i - 1].OpCode != OpCodes.Sub)
continue;
RewriteToNop (i - 5);
RewriteToNop (i - 4);
RewriteToNop (i - 3);
RewriteCondition (i, instr, opint3 - offset);
changed = true;
continue;
}
continue;
}
throw new NotImplementedException ();
}
// Mode special for csc in debug mode
switch (instr.OpCode.Code) {
case Code.Ceq:
case Code.Clt:
case Code.Cgt:
if (!GetOperandsConstantValues (i, out left, out right))
continue;
if (left is int lint && right is int rint) {
if (IsJumpTargetRange (i - 1, i))
continue;
RewriteToNop (i - 2);
RewriteToNop (i - 1);
if (IsComparisonAlwaysTrue (instr.OpCode, lint, rint)) {
Rewrite (i, Instruction.Create (OpCodes.Ldc_I4_1));
} else {
Rewrite (i, Instruction.Create (OpCodes.Ldc_I4_0));
}
changed = true;
}
break;
case Code.Cgt_Un:
if (!GetOperandsConstantValues (i, out left, out right))
continue;
if (IsJumpTargetRange (i - 1, i))
continue;
if (left == null && right == null) {
Rewrite (i, Instruction.Create (OpCodes.Ldc_I4_0));
}
changed = true;
break;
}
}
return changed;
}
BitArray GetReachableInstructionsMap (out List<ExceptionHandler>? unreachableHandlers)
{
Debug.Assert (FoldedInstructions != null);
unreachableHandlers = null;
var reachable = new BitArray (FoldedInstructions.Count);
Stack<int>? condBranches = null;
bool exceptionHandlersChecked = !Body.HasExceptionHandlers;
Instruction target;
int i = 0;
while (true) {
while (i < FoldedInstructions.Count) {
if (reachable[i])
break;
reachable[i] = true;
var instr = FoldedInstructions[i++];
switch (instr.OpCode.FlowControl) {
case FlowControl.Branch:
target = (Instruction) instr.Operand;
i = GetInstructionIndex (target);
continue;
case FlowControl.Cond_Branch:
condBranches ??= new Stack<int> ();
switch (instr.Operand) {
case Instruction starget:
condBranches.Push (GetInstructionIndex (starget));
continue;
case Instruction[] mtargets:
foreach (var t in mtargets)
condBranches.Push (GetInstructionIndex (t));
continue;
default:
throw new NotImplementedException ();
}
case FlowControl.Next:
case FlowControl.Call:
case FlowControl.Meta:
continue;
case FlowControl.Return:
case FlowControl.Throw:
break;
default:
throw new NotImplementedException ();
}
break;
}
if (condBranches?.Count > 0) {
i = condBranches.Pop ();
continue;
}
if (!exceptionHandlersChecked) {
exceptionHandlersChecked = true;
var instrs = Instructions;
foreach (var handler in ExceptionHandlers) {
int start = instrs.IndexOf (handler.TryStart);
int end = instrs.IndexOf (handler.TryEnd) - 1;
if (!HasAnyBitSet (reachable, start, end)) {
unreachableHandlers ??= new List<ExceptionHandler> ();
unreachableHandlers.Add (handler);
continue;
}
condBranches ??= new Stack<int> ();
condBranches.Push (GetInstructionIndex (handler.HandlerStart));
if (handler.FilterStart != null) {
condBranches.Push (GetInstructionIndex (handler.FilterStart));
int filterEnd = GetInstructionIndex (handler.HandlerStart) - 1;
if (filterEnd >= 0 && FoldedInstructions[filterEnd].OpCode == OpCodes.Endfilter) {
// The endfilter instruction must be at the end of each filter block, even if it's not reachable:
//
// ECMA 335
// I.12.4.2.8.2.5 endfilter:
// 1.Shall appear as the lexically last instruction in the filter.
// [Note: The endfilter is required even if no control - flow path reaches it.This can happen if, for
// example, the filter does a throw.end note]
reachable[filterEnd] = true;
}
}
}
if (condBranches?.Count > 0) {
i = condBranches.Pop ();
continue;
}
}
return reachable;
}
}
static bool HasAnyBitSet (BitArray bitArray, int startIndex, int endIndex)
{
for (int i = startIndex; i <= endIndex; ++i) {
if (bitArray[i])
return true;
}
return false;
}
//
// Returns index of instruction in folded instruction body
//
int GetInstructionIndex (Instruction instruction)
{
Debug.Assert (FoldedInstructions != null && mapping != null);
if (mapping.TryGetValue (instruction, out int idx))
return idx;
idx = FoldedInstructions.IndexOf (instruction);
Debug.Assert (idx >= 0);
return idx;
}
int? TryGetInstructionIndex (Instruction instruction)
{
Debug.Assert (mapping != null);
if (mapping.TryGetValue (instruction, out int idx))
return idx;
return null;
}
bool GetOperandsConstantValues (int index, out object? left, out object? right)
{
Debug.Assert (FoldedInstructions != null);
left = default;
right = default;
if (index < 2)
return false;
return GetConstantValue (FoldedInstructions[index - 2], out left) &&
GetConstantValue (FoldedInstructions[index - 1], out right);
}
static bool IsPairedStlocLdloc (Instruction first, Instruction second)
{
switch (first.OpCode.Code) {
case Code.Stloc_0:
return second.OpCode.Code == Code.Ldloc_0;
case Code.Stloc_1:
return second.OpCode.Code == Code.Ldloc_1;
case Code.Stloc_2:
return second.OpCode.Code == Code.Ldloc_2;
case Code.Stloc_3:
return second.OpCode.Code == Code.Ldloc_3;
case Code.Stloc_S:
case Code.Stloc:
if (second.OpCode.Code == Code.Ldloc_S || second.OpCode.Code == Code.Ldloc)
return ((VariableDefinition) first.Operand).Index == ((VariableDefinition) second.Operand).Index;
break;
}
return false;
}
bool IsJumpTargetRange (int firstInstr, int lastInstr)
{
Debug.Assert (FoldedInstructions != null);
return HasJumpIntoTargetRange (FoldedInstructions, firstInstr, lastInstr, TryGetInstructionIndex);
}
}
struct BodySweeper
{
readonly MethodBody body;
#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
Collection<Instruction> Instructions => body.Instructions;
Collection<VariableDefinition> Variables => body.Variables;
Collection<ExceptionHandler> ExceptionHandlers => body.ExceptionHandlers;
#pragma warning restore RS0030
readonly BitArray reachable;
readonly List<ExceptionHandler>? unreachableExceptionHandlers;
readonly LinkContext context;
LinkerILProcessor? ilprocessor;
LinkerILProcessor ILProcessor {
get {
Debug.Assert (ilprocessor != null);
return ilprocessor;
}
}
public BodySweeper (MethodBody body, BitArray reachable, List<ExceptionHandler>? unreachableEH, LinkContext context)
{
this.body = body;
this.reachable = reachable;
this.unreachableExceptionHandlers = unreachableEH;
this.context = context;
InstructionsReplaced = 0;
ilprocessor = null;
}
public int InstructionsReplaced { get; set; }
public void Initialize ()
{
var instrs = Instructions;
//
// Reusing same reachable map and altering it at indexes
// which will remain same during replacement processing
//
for (int i = 0; i < instrs.Count; ++i) {
if (reachable[i])
continue;
var instr = instrs[i];
switch (instr.OpCode.Code) {
case Code.Nop:
reachable[i] = true;
continue;
case Code.Ret:
if (i == instrs.Count - 1)
reachable[i] = true;
break;
}
}
ilprocessor = body.GetLinkerILProcessor ();
}
public void Process (List<int>? conditionInstrsToRemove, List<(int, Instruction)>? conditionInstrsToReplace, out List<Instruction>? sentinelNops)
{
List<VariableDefinition>? removedVariablesReferences = null;
var instrs = Instructions;
//
// Process list of conditional instructions that were set to be replaced and not removed
//
if (conditionInstrsToReplace != null) {
foreach (var pair in conditionInstrsToReplace) {
var instr = instrs[pair.Item1];
switch (instr.OpCode.StackBehaviourPop) {
case StackBehaviour.Popi:
ILProcessor.Replace (pair.Item1, pair.Item2);
InstructionsReplaced++;
break;
default:
Debug.Fail ("not supported");
break;
}
}
}
//
// Initial pass which replaces unreachable instructions with nops or
// ret to keep the body verifiable
//
for (int i = 0; i < instrs.Count; ++i) {
if (reachable[i])
continue;
var instr = instrs[i];
Instruction newInstr;
if (i == instrs.Count - 1) {
newInstr = Instruction.Create (OpCodes.Ret);
} else {
newInstr = Instruction.Create (OpCodes.Nop);
}
ILProcessor.Replace (i, newInstr);
InstructionsReplaced++;
VariableDefinition? variable = GetVariableReference (instr);
if (variable != null) {
removedVariablesReferences ??= new List<VariableDefinition> ();
if (!removedVariablesReferences.Contains (variable))
removedVariablesReferences.Add (variable);
}
}
CleanExceptionHandlers ();
sentinelNops = null;
//
// Process list of conditional jump which should be removed. They cannot be
// replaced with nops as they alter the stack
//
if (conditionInstrsToRemove != null) {
int bodyExpansion = 0;
foreach (int instrIndex in conditionInstrsToRemove) {
var index = instrIndex + bodyExpansion;
var instr = instrs[index];
switch (instr.OpCode.StackBehaviourPop) {
case StackBehaviour.Pop1_pop1:
InstructionsReplaced += 2;
//
// One of the operands is most likely constant and could just be removed instead of additional pop
//
if (index > 0 && IsSideEffectFreeLoad (instrs[index - 1])) {
var nop = Instruction.Create (OpCodes.Nop);
sentinelNops ??= new List<Instruction> ();
sentinelNops.Add (nop);
ILProcessor.Replace (index - 1, Instruction.Create (OpCodes.Pop));
ILProcessor.Replace (index, nop);
} else {
var pop = Instruction.Create (OpCodes.Pop);
ILProcessor.Replace (index, pop);
ILProcessor.InsertAfter (pop, Instruction.Create (OpCodes.Pop));
//
// conditionInstrsToRemove is always sorted and instead of
// increasing remaining indexes we introduce index delta value
//
bodyExpansion++;
}
break;
case StackBehaviour.Popi:
ILProcessor.Replace (index, Instruction.Create (OpCodes.Pop));
InstructionsReplaced++;
break;
}
}
}
//
// Replacing instructions with nops can make local variables unused. Process them
// as the last step to reduce more type dependencies
//
if (removedVariablesReferences != null) {
CleanRemovedVariables (removedVariablesReferences);
}
}
void CleanRemovedVariables (List<VariableDefinition> variables)
{
foreach (var instr in Instructions) {
VariableDefinition? variable = GetVariableReference (instr);
if (variable == null)
continue;
if (!variables.Remove (variable))
continue;
if (variables.Count == 0)
return;
}
variables.Sort ((a, b) => b.Index.CompareTo (a.Index));
var body_variables = Variables;
foreach (var variable in variables) {
var index = body_variables.IndexOf (variable);
//
// Remove variable only if it's the last one. Instead of
// re-indexing all variables change it to System.Object,
// which is enough to drop the dependency
//
if (index == body_variables.Count - 1) {
body_variables.RemoveAt (index);
} else {
var objectType = BCL.FindPredefinedType (WellKnownType.System_Object, context);
body_variables[index].VariableType = objectType ?? throw new NotSupportedException ("Missing predefined 'System.Object' type");
}
}
}
void CleanExceptionHandlers ()
{
if (unreachableExceptionHandlers == null)
return;
foreach (var eh in unreachableExceptionHandlers)
ExceptionHandlers.Remove (eh);
}
VariableDefinition? GetVariableReference (Instruction instruction)
{
switch (instruction.OpCode.Code) {
case Code.Stloc_0:
case Code.Ldloc_0:
return Variables[0];
case Code.Stloc_1:
case Code.Ldloc_1:
return Variables[1];
case Code.Stloc_2:
case Code.Ldloc_2:
return Variables[2];
case Code.Stloc_3:
case Code.Ldloc_3:
return Variables[3];
}
if (instruction.Operand is VariableReference vr)
return vr.Resolve ();
return null;
}
}
struct ConstantExpressionMethodAnalyzer
{
readonly LinkContext context;
readonly UnreachableBlocksOptimizer optimizer;
Stack<Instruction>? stack_instr;
Dictionary<int, Instruction>? locals;
public ConstantExpressionMethodAnalyzer (UnreachableBlocksOptimizer optimizer)
{
this.optimizer = optimizer;
this.context = optimizer._context;
stack_instr = null;
locals = null;
Result = null;
SideEffectFreeResult = true;
}
//
// Single expression that is representing the evaluation result with the specific
// callee arguments
//
public Instruction? Result { get; private set; }
//
// Returns true when the method evaluation with specific arguments does not cause
// any observable side effect (e.g. possible NRE, field access, etc)
//
public bool SideEffectFreeResult { get; private set; }
[MemberNotNullWhen (true, nameof(Result))]
public bool Analyze (in CalleePayload callee, Stack<MethodDefinition> callStack)
{
MethodDefinition method = callee.Method;
Instruction[]? arguments = callee.Arguments;
#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
Collection<Instruction> instructions = callee.Method.Body.Instructions;
#pragma warning restore RS0030
MethodBody body = method.Body;
VariableReference vr;
Instruction? jmpTarget = null;
Instruction? linstr;
object? left, right, operand;
SideEffectFreeResult = !HasSideEffects (method);
//
// We could implement a full-blown interpreter here but for now, it handles
// cases used in runtime libraries
//
for (int i = 0; i < instructions.Count; ++i) {
var instr = instructions[i];
if (jmpTarget != null) {
//
// Handles both backward and forward jumps
//
if (instr != jmpTarget)
continue;
jmpTarget = null;
}
switch (instr.OpCode.Code) {
case Code.Nop:
case Code.Volatile:
continue;
case Code.Pop:
Debug.Assert (stack_instr != null, "invalid il?");
stack_instr?.Pop ();
continue;
case Code.Br_S:
case Code.Br:
jmpTarget = (Instruction) instr.Operand;
continue;
case Code.Brfalse_S:
case Code.Brfalse: {
if (!GetOperandConstantValue (out operand))
return false;
if (operand is int oint) {
if (oint == 0)
jmpTarget = (Instruction) instr.Operand;
continue;
}
return false;
}
case Code.Brtrue_S:
case Code.Brtrue: {
if (!GetOperandConstantValue (out operand))
return false;
if (operand is int oint) {
if (oint != 0)
jmpTarget = (Instruction) instr.Operand;
continue;
}
return false;
}
case Code.Beq:
case Code.Beq_S:
case Code.Bne_Un:
case Code.Bne_Un_S:
case Code.Bge:
case Code.Bge_S:
case Code.Bge_Un:
case Code.Bge_Un_S:
case Code.Bgt:
case Code.Bgt_S:
case Code.Bgt_Un:
case Code.Bgt_Un_S:
case Code.Ble:
case Code.Ble_S:
case Code.Ble_Un:
case Code.Ble_Un_S:
case Code.Blt:
case Code.Blt_S:
case Code.Blt_Un:
case Code.Blt_Un_S:
if (EvaluateConditionalJump (instr, out jmpTarget))
continue;
return false;
case Code.Ldc_I4:
case Code.Ldc_I4_S:
case Code.Ldc_I4_0:
case Code.Ldc_I4_1:
case Code.Ldc_I4_2:
case Code.Ldc_I4_3:
case Code.Ldc_I4_4:
case Code.Ldc_I4_5:
case Code.Ldc_I4_6:
case Code.Ldc_I4_7:
case Code.Ldc_I4_8:
case Code.Ldc_I4_M1:
case Code.Ldc_I8:
case Code.Ldnull:
case Code.Ldstr:
case Code.Ldtoken:
PushOnStack (instr);
continue;
case Code.Ldloc_0:
linstr = GetLocalsValue (0, body);
if (linstr == null)
return false;
PushOnStack (linstr);
continue;
case Code.Ldloc_1:
linstr = GetLocalsValue (1, body);
if (linstr == null)
return false;
PushOnStack (linstr);
continue;
case Code.Ldloc_2:
linstr = GetLocalsValue (2, body);
if (linstr == null)
return false;
PushOnStack (linstr);
continue;
case Code.Ldloc_3:
linstr = GetLocalsValue (3, body);
if (linstr == null)
return false;
PushOnStack (linstr);
continue;
case Code.Ldloc:
case Code.Ldloc_S:
vr = (VariableReference) instr.Operand;
linstr = GetLocalsValue (vr.Index, body);
if (linstr == null)
return false;
PushOnStack (linstr);
continue;
case Code.Stloc_0:
StoreToLocals (0);
continue;
case Code.Stloc_1:
StoreToLocals (1);
continue;
case Code.Stloc_2:
StoreToLocals (2);
continue;
case Code.Stloc_3:
StoreToLocals (3);
continue;
case Code.Stloc_S:
case Code.Stloc:
vr = (VariableReference) instr.Operand;
StoreToLocals (vr.Index);
continue;
case Code.Ldarg_0:
if (!method.IsStatic) {
PushOnStack (instr);
continue;
}
linstr = GetArgumentValue (arguments, 0);
if (linstr == null)
return false;
PushOnStack (linstr);
continue;
case Code.Ldarg_1:
if (!method.IsStatic)
return false;
linstr = GetArgumentValue (arguments, 1);
if (linstr == null)
return false;
PushOnStack (linstr);
continue;
case Code.Ldsfld: {
var ftarget = (FieldReference) instr.Operand;
FieldDefinition? field = context.TryResolve (ftarget);
if (field == null)
return false;
if (context.Annotations.TryGetFieldUserValue (field, out object? value)) {
linstr = CodeRewriterStep.CreateConstantResultInstruction (context, field.FieldType, value);
if (linstr == null)
return false;
} else {
SideEffectFreeResult = false;
linstr = instr;
}
PushOnStack (linstr);
continue;
}
case Code.Ceq: {
if (!GetOperandsConstantValues (out right, out left))
return false;
if (left is int lint && right is int rint) {
PushOnStack (Instruction.Create (OpCodes.Ldc_I4, lint == rint ? 1 : 0));
continue;
}
if (left is long llong && right is long rlong) {
PushOnStack (Instruction.Create (OpCodes.Ldc_I4, llong == rlong ? 1 : 0));
continue;
}
return false;
}
case Code.Conv_I8: {
if (!GetOperandConstantValue (out operand))
return false;
if (operand is int oint) {
PushOnStack (Instruction.Create (OpCodes.Ldc_I8, (long) oint));
continue;
}
// TODO: Handle more types
return false;
}
case Code.Call:
case Code.Callvirt: {
MethodReference mr = (MethodReference) instr.Operand;
MethodDefinition? md = optimizer._context.TryResolve (mr);
if (md == null || md == method)
return false;
if (md.IsVirtual)
return false;
Instruction[]? args;
if (!md.HasMetadataParameters ()) {
args = Array.Empty<Instruction> ();
} else {
//
// Don't need to check for ref/out because ldloca like instructions are not supported
//
args = GetArgumentsOnStack (md);
if (args == null)
return false;
}
if (md.ReturnType.MetadataType == MetadataType.Void) {
// For now consider all void methods as side-effect causing
SideEffectFreeResult = false;
continue;
}
if (!md.IsStatic && !CanEvaluateInstanceMethodCall (method))
return false;
//
// Evaluate known framework methods
//
if (args.Length > 0) {
linstr = EvaluateIntrinsicCall (md, args);
if (linstr != null) {
PushOnStack (linstr);
continue;
}
}
//
// Guard against stack overflow on recursive calls. This could be turned into
// a warning if we check arguments too
//
if (callStack.Contains (md))
return false;
callStack.Push (method);
MethodResult? call_result = optimizer.TryGetMethodCallResult (new CalleePayload (md, args), callStack);
if (!callStack.TryPop (out _))
return false;
if (call_result is MethodResult result) {
if (!result.IsSideEffectFree)
SideEffectFreeResult = false;
PushOnStack (result.Instruction);
continue;
}
return false;
}
case Code.Sizeof: {
var type = (TypeReference) instr.Operand;
linstr = optimizer.GetSizeOfResult (type);
if (linstr != null) {
PushOnStack (linstr);
continue;
}
return false;
}
case Code.Ret:
if (ConvertStackToResult ())
return true;
break;
}
return false;
}
return false;
}
bool CanEvaluateInstanceMethodCall (MethodDefinition context)
{
if (stack_instr == null || !stack_instr.TryPop (out Instruction? instr))
return false;
switch (instr.OpCode.Code) {
case Code.Ldarg_0:
if (!context.IsStatic)
return true;
goto default;
default:
// We are not inlining hence can evaluate anything and decide later
// how to handle sitation when the result is not deterministic
SideEffectFreeResult = false;
return true;
}
}
bool EvaluateConditionalJump (Instruction instr, out Instruction? target)
{
if (!GetOperandsConstantValues (out object? right, out object? left)) {
target = null;
return false;
}
if (left is int lint && right is int rint) {
if (IsComparisonAlwaysTrue (instr.OpCode, lint, rint))
target = (Instruction) instr.Operand;
else
target = null;
return true;
}
target = null;
return false;
}
[MemberNotNullWhen (true, nameof(Result))]
bool ConvertStackToResult ()
{
if (stack_instr == null)
return false;
if (stack_instr.Count != 1)
return false;
var instr = stack_instr.Pop ();
switch (instr.OpCode.Code) {
case Code.Ldc_I4_0:
case Code.Ldc_I4_1:
case Code.Ldc_I4_2:
case Code.Ldc_I4_3:
case Code.Ldc_I4_4:
case Code.Ldc_I4_5:
case Code.Ldc_I4_6:
case Code.Ldc_I4_7:
case Code.Ldc_I4_8:
case Code.Ldc_I4:
case Code.Ldc_I4_S:
case Code.Ldc_I4_M1:
case Code.Ldc_I8:
case Code.Ldnull:
case Code.Ldstr:
Result = instr;
return true;
}
return false;
}
static Instruction? GetArgumentValue (Instruction[]? arguments, int index)
{
if (arguments == null)
return null;
return index < arguments.Length ? arguments[index] : null;
}
Instruction[]? GetArgumentsOnStack (MethodDefinition method)
{
int length = method.GetMetadataParametersCount ();
Debug.Assert (length != 0);
if (stack_instr?.Count < length)
return null;
var result = new Instruction[length];
while (length != 0)
result[--length] = stack_instr!.Pop ();
return result;
}
Instruction? GetLocalsValue (int index, MethodBody body)
{
if (locals != null && locals.TryGetValue (index, out Instruction? instruction))
return instruction;
if (!body.InitLocals)
return null;
#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
var variables = body.Variables;
#pragma warning restore RS0030
// local variables don't need to be explicitly initialized
return CodeRewriterStep.CreateConstantResultInstruction (context, variables[index].VariableType);
}
bool GetOperandConstantValue ([NotNullWhen (true)] out object? value)
{
if (stack_instr == null) {
value = null;
return false;
}
Instruction? instr;
if (!stack_instr.TryPop (out instr)) {
value = null;
return false;
}
return GetConstantValue (instr, out value);
}
bool GetOperandsConstantValues ([NotNullWhen (true)] out object? left, [NotNullWhen (true)] out object? right)
{
if (stack_instr == null) {
left = right = null;
return false;
}
Instruction? instr;
if (!stack_instr.TryPop (out instr)) {
left = right = null;
return false;
}
if (instr == null) {
left = right = null;
return false;
}
if (!GetConstantValue (instr, out left)) {
left = right = null;
return false;
}
if (!stack_instr.TryPop (out instr)) {
left = right = null;
return false;
}
if (instr is null) {
left = right = null;
return false;
}
return GetConstantValue (instr, out right);
}
void PushOnStack (Instruction instruction)
{
stack_instr ??= new Stack<Instruction> ();
stack_instr.Push (instruction);
}
void StoreToLocals (int index)
{
locals ??= new Dictionary<int, Instruction> ();
if (stack_instr == null)
Debug.Fail ("Invalid IL?");
locals[index] = stack_instr.Pop ();
}
}
readonly record struct CalleePayload (MethodDefinition Method, Instruction[]? Arguments = null)
{
public bool HasUnknownArguments => Arguments is null;
}
readonly record struct MethodResult (Instruction Instruction, bool IsSideEffectFree)
{
public Instruction GetPrototype () => Instruction.GetPrototype ();
}
}
}
|