mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
9.5 KiB
222 lines
9.5 KiB
// Copyright (c) 2026 Siegfried Pammer |
|
// |
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this |
|
// software and associated documentation files (the "Software"), to deal in the Software |
|
// without restriction, including without limitation the rights to use, copy, modify, merge, |
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
|
// to whom the Software is furnished to do so, subject to the following conditions: |
|
// |
|
// The above copyright notice and this permission notice shall be included in all copies or |
|
// substantial portions of the Software. |
|
// |
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|
// DEALINGS IN THE SOFTWARE. |
|
|
|
|
|
using System.Collections.Generic; |
|
using System.Linq; |
|
|
|
using ICSharpCode.Decompiler.TypeSystem; |
|
|
|
namespace ICSharpCode.Decompiler.IL.Transforms |
|
{ |
|
/// <summary> |
|
/// Expands a statement-level assignment of a nested conditional operator back into if-else. |
|
/// |
|
/// `ExpressionTransforms.HandleConditionalOperator` collapses `if (c) a = x; else a = y;` into a |
|
/// conditional operator, innermost first, and keeps going for as long as the chain does. A source |
|
/// else-if ladder therefore comes back as one expression, however long it was: the sample in |
|
/// issue #2027 decompiles to a single 2095-character statement. |
|
/// |
|
/// This undoes the collapse past <see cref="MaxNesting"/> levels, so a statement keeps at most |
|
/// that many conditional operators. |
|
/// </summary> |
|
/// <remarks> |
|
/// This runs at the end of the pipeline, not inside ExpressionTransforms, because every transform |
|
/// that needs its input to be a single expression has to see the collapsed form first: object and |
|
/// collection initializers, `with`, switch expressions, interpolated string handlers, and the |
|
/// query lambdas the C# stage later rewrites into clauses. Cutting the chain earlier leaves an |
|
/// if-else between the statements they pattern-match on, and they silently stop matching. |
|
/// |
|
/// Only a store to a declared local is expanded. Such a local carries its own type from metadata, |
|
/// so the branches can be split without the result widening to the stack type - which is what |
|
/// makes a separate type check unnecessary here. |
|
/// </remarks> |
|
public class ExpandNestedConditionals : IILTransform |
|
{ |
|
/// <summary> |
|
/// How many conditional operators a single statement may keep. |
|
/// </summary> |
|
const int MaxNesting = 1; |
|
|
|
ILTransformContext context; |
|
|
|
public void Run(ILFunction function, ILTransformContext context) |
|
{ |
|
this.context = context; |
|
if (context.Settings.AggressiveInlining) |
|
return; |
|
foreach (var f in function.Descendants.OfType<ILFunction>()) |
|
{ |
|
if (!IsExpandableFunction(f)) |
|
continue; |
|
foreach (var block in f.Descendants.OfType<Block>().ToArray()) |
|
{ |
|
if (block.Ancestors.OfType<ILFunction>().FirstOrDefault() != f) |
|
continue; |
|
if (!IsExpandableBlock(block)) |
|
continue; |
|
for (int i = 0; i < block.Instructions.Count; i++) |
|
{ |
|
context.CancellationToken.ThrowIfCancellationRequested(); |
|
ExtractInto(block, i); |
|
Expand(f, block, i); |
|
} |
|
} |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// A lambda body may have to stay a single expression: the query-expression stage rewrites |
|
/// one into a clause, and an expression tree is built from the expression itself. A |
|
/// constructor is matched as a whole, so that its leading stores become field initializers |
|
/// and a record's primary constructor is recognized. |
|
/// </summary> |
|
static bool IsExpandableFunction(ILFunction function) |
|
{ |
|
return function.Kind is ILFunctionKind.TopLevelFunction or ILFunctionKind.LocalFunction |
|
&& function.Method?.IsConstructor != true; |
|
} |
|
|
|
/// <summary> |
|
/// Only plain control-flow blocks hold statements. The other block kinds are expressions |
|
/// spelled as blocks - an initializer, a named-argument call - and an if-else inside one of |
|
/// them is not a statement the C# stage can print. |
|
/// </summary> |
|
static bool IsExpandableBlock(Block block) |
|
{ |
|
if (block.Kind != BlockKind.ControlFlow) |
|
return false; |
|
if (ILInlining.IsCatchWhenBlock(block)) |
|
return false; |
|
// A branch body is a block hanging off the if, not off a container; it holds statements |
|
// either way. Only a container's own layout reserves blocks for a loop or switch header. |
|
if (block.Parent is not BlockContainer container) |
|
return true; |
|
return container.Kind switch { |
|
// the entry point carries the loop condition or the switch value |
|
ContainerKind.While or ContainerKind.Switch => block != container.EntryPoint, |
|
// and for a for-loop the last block carries the increment |
|
ContainerKind.For => block != container.EntryPoint |
|
&& block != container.Blocks[container.Blocks.Count - 1], |
|
ContainerKind.DoWhile => block != container.Blocks[container.Blocks.Count - 1], |
|
_ => true, |
|
}; |
|
} |
|
|
|
/// <summary> |
|
/// A conditional that is not stored to a variable - an argument, a return value, a |
|
/// condition - has nothing to expand into. ILExtraction gives it one, vetting the move |
|
/// through PrepareExtract so the order of evaluation is preserved. |
|
/// </summary> |
|
/// <remarks> |
|
/// Extraction is only done where the position the value flows into names a type. The |
|
/// temporary ILExtraction creates is typed from the stack type, and `I4` is `int`, `bool`, |
|
/// `char` and every enum at once; the consumer knows better, because a parameter, a return |
|
/// type or a field carries its type in metadata. |
|
/// </remarks> |
|
void ExtractInto(Block block, int pos) |
|
{ |
|
if (block.Instructions[pos] is StLoc { Value: IfInstruction }) |
|
return; |
|
foreach (var inst in block.Instructions[pos].Descendants.OfType<IfInstruction>().ToArray()) |
|
{ |
|
if (inst.Parent is StLoc || !ExceedsNesting(inst, MaxNesting)) |
|
continue; |
|
var expected = inst.Parent?.InferExpectedType(inst.ChildIndex, context.TypeSystem); |
|
if (expected == null || expected.Kind == TypeKind.Unknown |
|
|| expected.GetStackType() != inst.ResultType) |
|
{ |
|
continue; |
|
} |
|
context.Step("Extract nested conditional operator", inst); |
|
var v = inst.Extract(context); |
|
if (v != null) |
|
{ |
|
v.Type = expected; |
|
context.EndStep(block.Instructions[pos]); |
|
return; |
|
} |
|
context.EndStep(inst); |
|
} |
|
} |
|
|
|
void Expand(ILFunction function, Block block, int pos) |
|
{ |
|
// Only one branch of a chain nests further, so the expansion walks down it. A chain is |
|
// bounded only by the size of the method it came from, hence the loop over recursion. |
|
var worklist = new Stack<(Block Block, int Pos)>(); |
|
worklist.Push((block, pos)); |
|
while (worklist.Count > 0) |
|
{ |
|
var (current, index) = worklist.Pop(); |
|
if (current.Instructions[index] is not StLoc { Variable: var v, Value: IfInstruction ifInst } stloc) |
|
continue; |
|
if (v.Kind is not (VariableKind.Local or VariableKind.StackSlot) || v.Type is ByReferenceType) |
|
continue; |
|
if (ILInlining.IsInConstructorInitializer(function, stloc)) |
|
continue; |
|
if (!ExceedsNesting(ifInst, MaxNesting)) |
|
continue; |
|
|
|
context.Step("Expand nested conditional operator", stloc); |
|
// HandleConditionalOperator built the conditional by negating the condition and |
|
// swapping the branches; undoing that here keeps the source's own polarity, and |
|
// makes the expansion the exact inverse of the collapse rather than an equivalent |
|
// of it - without which a pretty-test fixture would never reach a fixed point. |
|
var condition = ifInst.Condition; |
|
var (trueValue, falseValue) = (ifInst.TrueInst, ifInst.FalseInst); |
|
while (condition.MatchLogicNot(out var withoutNot)) |
|
{ |
|
condition = withoutNot; |
|
(trueValue, falseValue) = (falseValue, trueValue); |
|
} |
|
// Each new store stands where its value did, so the statement keeps a sequence point |
|
// of its own instead of inheriting the one the value already carries. |
|
var trueStore = new StLoc(v, trueValue).WithILRange(trueValue); |
|
var falseStore = new StLoc(v, falseValue).WithILRange(falseValue); |
|
var trueBlock = new Block { Instructions = { trueStore } }.WithILRange(trueStore); |
|
var falseBlock = new Block { Instructions = { falseStore } }.WithILRange(falseStore); |
|
var expanded = new IfInstruction(condition, trueBlock, falseBlock); |
|
expanded.AddILRange(ifInst); |
|
expanded.AddILRange(stloc); |
|
stloc.ReplaceWith(expanded); |
|
context.EndStep(expanded); |
|
|
|
// the branches are statements of their own now, so each may need expanding in turn |
|
worklist.Push((trueBlock, 0)); |
|
worklist.Push((falseBlock, 0)); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Gets whether the branches of <paramref name="inst"/> nest conditional operators |
|
/// <paramref name="depth"/> levels deep. The short-circuit logic operators share the |
|
/// IfInstruction representation, but render as operator chains without visible nesting. |
|
/// </summary> |
|
static bool ExceedsNesting(ILInstruction inst, int depth) |
|
{ |
|
if (inst is not IfInstruction ifInst) |
|
return false; |
|
if (ifInst.MatchLogicAnd(out _, out _) || ifInst.MatchLogicOr(out _, out _)) |
|
return false; |
|
return depth <= 0 |
|
|| ExceedsNesting(ifInst.TrueInst, depth - 1) |
|
|| ExceedsNesting(ifInst.FalseInst, depth - 1); |
|
} |
|
} |
|
}
|
|
|