Files
Daniel Grunwald 5526400b17 Extend InferType() to every ILInstruction
Change InferType() to an abstract method and implement for every ILInstruction.
With this change, we now always have enough information to create a variable of an appropriate type to store the result of evaluating the instruction.
This previously was not the case for instructions producing "other value type", for which the stacktype-based fallback incorrectly produced `object`.
2026-08-30 21:25:45 +02:00

223 lines
9.5 KiB
C#

// 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, context.TypeSystem.FindType(KnownTypeCode.Void));
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);
}
}
}