Skip to content

Commit dd4bf7d

Browse files
committed
Fix #3518 by replacing FixLoneIsInst with an inlining restriction.
This way we avoid having to extract later, as we will never inline if the `isinst` argument if this could result in it being unrepresentable in C#. This commit also refactors inlining restrictions to avoid requiring special cases in ILInlining itself. But when making this change, I discovered that this broke our pattern-matching tests, and that the weird IL with double `isinst` is indeed generated by the C# compiler for `if (genericParam is StringComparison.Ordinal)` style code. So instead we also allow `isinst` with a `box(expr-without-side-effects)` argument to be represented with the `expr is T ? (T)expr : null` emulation.
1 parent 32cb515 commit dd4bf7d

File tree

10 files changed

+154
-142
lines changed

10 files changed

+154
-142
lines changed

ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ public static List<IILTransform> GetILTransforms()
158158
},
159159
new ProxyCallReplacer(),
160160
new FixRemainingIncrements(),
161-
new FixLoneIsInst(),
162161
new CopyPropagation(),
163162
new DelegateConstruction(),
164163
new LocalFunctionDecompiler(),

ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -398,10 +398,12 @@ protected internal override TranslatedExpression VisitIsInst(IsInst inst, Transl
398398
// unbox.any T(isinst T(expr)) ==> "expr as T" for nullable value types and class-constrained generic types
399399
// comp(isinst T(expr) != null) ==> "expr is T"
400400
// on block level (StatementBuilder.VisitIsInst) => "expr is T"
401-
if (SemanticHelper.IsPure(inst.Argument.Flags))
401+
if (SemanticHelper.IsPure(inst.Argument.Flags) || (inst.Argument is Box box && SemanticHelper.IsPure(box.Argument.Flags)))
402402
{
403403
// We can emulate isinst using
404404
// expr is T ? expr : null
405+
// (doubling the boxing side-effect is harmless because the "expr is T" part won't observe object identity,
406+
// and we need to support this because Roslyn pattern matching sometimes generates such code.)
405407
return new ConditionalExpression(
406408
new IsExpression(arg, ConvertType(inst.Type)).WithILInstruction(inst),
407409
arg.Expression.Clone(),
@@ -3185,16 +3187,16 @@ TranslatedExpression ConvertArrayIndex(TranslatedExpression input, StackType sta
31853187
return input.ConvertTo(targetType, this);
31863188
}
31873189

3188-
internal static bool IsUnboxAnyWithIsInst(UnboxAny unboxAny, IsInst isInst)
3190+
internal static bool IsUnboxAnyWithIsInst(UnboxAny unboxAny, IType isInstType)
31893191
{
3190-
return unboxAny.Type.Equals(isInst.Type)
3191-
&& (unboxAny.Type.IsKnownType(KnownTypeCode.NullableOfT) || isInst.Type.IsReferenceType == true);
3192+
return unboxAny.Type.Equals(isInstType)
3193+
&& (unboxAny.Type.IsKnownType(KnownTypeCode.NullableOfT) || isInstType.IsReferenceType == true);
31923194
}
31933195

31943196
protected internal override TranslatedExpression VisitUnboxAny(UnboxAny inst, TranslationContext context)
31953197
{
31963198
TranslatedExpression arg;
3197-
if (inst.Argument is IsInst isInst && IsUnboxAnyWithIsInst(inst, isInst))
3199+
if (inst.Argument is IsInst isInst && IsUnboxAnyWithIsInst(inst, isInst.Type))
31983200
{
31993201
// unbox.any T(isinst T(expr)) ==> expr as T
32003202
// This is used for generic types and nullable value types

ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@
136136
<Compile Include="Humanizer\Vocabulary.cs" />
137137
<Compile Include="IL\ControlFlow\RemoveRedundantReturn.cs" />
138138
<Compile Include="IL\Transforms\DeconstructionTransform.cs" />
139-
<Compile Include="IL\Transforms\FixLoneIsInst.cs" />
140139
<Compile Include="IL\Instructions\DeconstructInstruction.cs" />
141140
<Compile Include="IL\Instructions\DeconstructResultInstruction.cs" />
142141
<Compile Include="IL\Instructions\MatchInstruction.cs" />
@@ -413,6 +412,7 @@
413412
<Compile Include="IL\Instructions\LdFlda.cs" />
414413
<Compile Include="IL\Instructions\NullableInstructions.cs" />
415414
<Compile Include="IL\Instructions\StLoc.cs" />
415+
<Compile Include="IL\Instructions\IsInst.cs" />
416416
<Compile Include="DebugInfo\SequencePoint.cs" />
417417
<Compile Include="IL\Instructions\CallIndirect.cs" />
418418
<Compile Include="IL\Instructions\DefaultValue.cs" />

ICSharpCode.Decompiler/IL/ILReader.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,14 @@ DecodedInstruction DecodeInstruction()
11381138
case ILOpCode.Initobj:
11391139
return InitObj(PopStObjTarget(), ReadAndDecodeTypeReference());
11401140
case ILOpCode.Isinst:
1141-
return Push(new IsInst(Pop(StackType.O), ReadAndDecodeTypeReference()));
1141+
{
1142+
var type = ReadAndDecodeTypeReference();
1143+
if (type.IsReferenceType != true)
1144+
{
1145+
FlushExpressionStack(); // value-type isinst has inlining restrictions
1146+
}
1147+
return Push(new IsInst(Pop(StackType.O), type));
1148+
}
11421149
case ILOpCode.Ldelem:
11431150
return LdElem(ReadAndDecodeTypeReference());
11441151
case ILOpCode.Ldelem_i1:

ICSharpCode.Decompiler/IL/Instructions/Block.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,22 @@ public override StackType ResultType {
217217
}
218218
}
219219

220+
internal override bool CanInlineIntoSlot(int childIndex, ILInstruction expressionBeingMoved)
221+
{
222+
switch (Kind)
223+
{
224+
case BlockKind.ControlFlow when Parent is BlockContainer:
225+
case BlockKind.ArrayInitializer:
226+
case BlockKind.CollectionInitializer:
227+
case BlockKind.ObjectInitializer:
228+
case BlockKind.CallInlineAssign:
229+
// Allow inlining into the first instruction of the block
230+
return childIndex == 0;
231+
default:
232+
return false;
233+
}
234+
}
235+
220236
/// <summary>
221237
/// Gets the name of this block.
222238
/// </summary>

ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -941,13 +941,28 @@ internal virtual bool PrepareExtract(int childIndex, Transforms.ExtractionContex
941941
}
942942

943943
/// <summary>
944-
/// Gets whether the specified instruction may be inlined into the specified slot.
945-
/// Note: this does not check whether reordering with the previous slots is valid; only wheter the target slot supports inlining at all!
944+
/// Gets whether the expressionBeingMoved, which previously executes prior to `this`,
945+
/// may be moved into a descendant of the specified slot.
946+
/// (i.e. this controls whether FindLoadInNext may descent into the specified slot)
947+
///
948+
/// Note: this does not check whether reordering with the previous slots is valid; only whether the target slot supports inlining at all!
946949
/// </summary>
947950
internal virtual bool CanInlineIntoSlot(int childIndex, ILInstruction expressionBeingMoved)
948951
{
949952
return GetChildSlot(childIndex).CanInlineInto;
950953
}
954+
955+
/// <summary>
956+
/// Some slots in the ILAst have restrictions on which instructions can appear in them.
957+
/// Used to suppress inlining if the new child does not satisfy the restrictions.
958+
/// Unlike `CanInlineIntoSlot`, this is not about descendants of the slot, only about
959+
/// whether SetChild(childIndex, newChild) is valid.
960+
/// (i.e. this controls whether FindLoadInNext may return the specified slot as a final result)
961+
/// </summary>
962+
internal virtual bool SatisfiesSlotRestrictionForInlining(int childIndex, ILInstruction newChild)
963+
{
964+
return true;
965+
}
951966
}
952967

953968
public interface IInstructionWithTypeOperand
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#nullable enable
2+
// Copyright (c) 2025 Daniel Grunwald
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
5+
// software and associated documentation files (the "Software"), to deal in the Software
6+
// without restriction, including without limitation the rights to use, copy, modify, merge,
7+
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
8+
// to whom the Software is furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in all copies or
11+
// substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
14+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
15+
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
16+
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
17+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
18+
// DEALINGS IN THE SOFTWARE.
19+
20+
using System.Diagnostics;
21+
22+
using ICSharpCode.Decompiler.CSharp;
23+
24+
namespace ICSharpCode.Decompiler.IL;
25+
26+
partial class IsInst
27+
{
28+
internal override bool SatisfiesSlotRestrictionForInlining(int childIndex, ILInstruction newChild)
29+
{
30+
Debug.Assert(childIndex == 0);
31+
Debug.Assert(base.SatisfiesSlotRestrictionForInlining(childIndex, newChild));
32+
if (this.Type.IsReferenceType == true)
33+
{
34+
return true; // reference-type isinst is always supported
35+
}
36+
if (SemanticHelper.IsPure(newChild.Flags))
37+
{
38+
return true; // emulated via "expr is T ? (T)expr : null"
39+
}
40+
else if (newChild is Box box && SemanticHelper.IsPure(box.Argument.Flags))
41+
{
42+
// Also emulated via "expr is T ? (T)expr : null".
43+
// This duplicates the boxing side-effect, but that's harmless as one of the boxes is only
44+
// used in the `expr is T` type test where the object identity can never be observed.
45+
// This appears as part of C# pattern matching, inlining early makes those code patterns easier to detect.
46+
return true;
47+
}
48+
if (this.Parent is UnboxAny unboxAny && ExpressionBuilder.IsUnboxAnyWithIsInst(unboxAny, this.Type))
49+
{
50+
return true; // supported pattern "expr as T?"
51+
}
52+
if (this.Parent != null && (this.Parent.MatchCompEqualsNull(out _) || this.Parent.MatchCompNotEqualsNull(out _)))
53+
{
54+
return true; // supported pattern "expr is T"
55+
}
56+
if (this.Parent is Block { Kind: BlockKind.ControlFlow })
57+
{
58+
return true; // supported via StatementBuilder.VisitIsInst
59+
}
60+
return false;
61+
}
62+
}

ICSharpCode.Decompiler/IL/Instructions/LdFlda.cs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,28 +48,35 @@ internal override void CheckInvariant(ILPhase phase)
4848

4949
public sealed partial class StObj
5050
{
51-
/// <summary>
52-
/// For a store to a field or array element, C# will only throw NullReferenceException/IndexOfBoundsException
53-
/// after the value-to-be-stored has been computed.
54-
/// This means a LdFlda/LdElema used as target for StObj must have DelayExceptions==true to allow a translation to C#
55-
/// without changing the program semantics. See https://github.com/icsharpcode/ILSpy/issues/2050
56-
/// </summary>
57-
public bool CanInlineIntoTargetSlot(ILInstruction inst)
51+
internal override bool SatisfiesSlotRestrictionForInlining(int childIndex, ILInstruction newChild)
5852
{
59-
switch (inst.OpCode)
53+
if (childIndex == 0) // target slot
6054
{
61-
case OpCode.LdElema:
62-
case OpCode.LdFlda:
63-
Debug.Assert(inst.HasDirectFlag(InstructionFlags.MayThrow));
64-
// If the ldelema/ldflda may throw a non-delayed exception, inlining will cause it
65-
// to turn into a delayed exception after the translation to C#.
66-
// This is only valid if the value computation doesn't involve any side effects.
67-
return SemanticHelper.IsPure(this.Value.Flags);
68-
// Note that after inlining such a ldelema/ldflda, the normal inlining rules will
69-
// prevent us from inlining an effectful instruction into the value slot.
70-
default:
71-
return true;
55+
Debug.Assert(GetChildSlot(childIndex) == TargetSlot);
56+
// For a store to a field or array element, C# will only throw NullReferenceException/IndexOfBoundsException
57+
// after the value-to-be-stored has been computed.
58+
// This means a LdFlda/LdElema used as target for StObj must have DelayExceptions==true to allow a translation to C#
59+
// without changing the program semantics. See https://github.com/icsharpcode/ILSpy/issues/2050
60+
switch (newChild.OpCode)
61+
{
62+
case OpCode.LdElema:
63+
case OpCode.LdFlda:
64+
{
65+
Debug.Assert(newChild.HasDirectFlag(InstructionFlags.MayThrow));
66+
// If the ldelema/ldflda may throw a non-delayed exception, inlining will cause it
67+
// to turn into a delayed exception after the translation to C#.
68+
// This is only valid if the value computation doesn't involve any side effects.
69+
if (!SemanticHelper.IsPure(this.Value.Flags))
70+
{
71+
return false;
72+
}
73+
// Note that after inlining such a ldelema/ldflda, the normal inlining rules will
74+
// prevent us from inlining an effectful instruction into the value slot.
75+
break;
76+
}
77+
}
7278
}
79+
return base.SatisfiesSlotRestrictionForInlining(childIndex, newChild);
7380
}
7481

7582
/// <summary>

ICSharpCode.Decompiler/IL/Transforms/FixLoneIsInst.cs

Lines changed: 0 additions & 69 deletions
This file was deleted.

ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs

Lines changed: 17 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -821,46 +821,27 @@ internal static FindResult FindLoadInNext(ILInstruction expr, ILVariable v, ILIn
821821
return FindResult.Stop;
822822
if (expr.MatchLdLoc(v) || expr.MatchLdLoca(v))
823823
{
824-
// Match found, we can inline
825-
if (expr.SlotInfo == StObj.TargetSlot && !((StObj)expr.Parent).CanInlineIntoTargetSlot(expressionBeingMoved))
824+
// Match found, we can inline unless there are slot restrictions.
825+
if (expr.Parent.SatisfiesSlotRestrictionForInlining(expr.ChildIndex, expressionBeingMoved))
826826
{
827-
if ((options & InliningOptions.AllowChangingOrderOfEvaluationForExceptions) != 0)
828-
{
829-
// Intentionally change code semantics so that we can avoid a ref local
830-
if (expressionBeingMoved is LdFlda ldflda)
831-
ldflda.DelayExceptions = true;
832-
else if (expressionBeingMoved is LdElema ldelema)
833-
ldelema.DelayExceptions = true;
834-
}
835-
else
836-
{
837-
// special case: the StObj.TargetSlot does not accept some kinds of expressions
838-
return FindResult.Stop;
839-
}
827+
return FindResult.Found(expr);
840828
}
841-
return FindResult.Found(expr);
842-
}
843-
else if (expr is Block block)
844-
{
845-
// Inlining into inline-blocks?
846-
switch (block.Kind)
829+
// We cannot inline because the targets slot restrictions are not satisfied
830+
if ((options & InliningOptions.AllowChangingOrderOfEvaluationForExceptions) != 0 && expr.SlotInfo == StObj.TargetSlot)
847831
{
848-
case BlockKind.ControlFlow when block.Parent is BlockContainer:
849-
case BlockKind.ArrayInitializer:
850-
case BlockKind.CollectionInitializer:
851-
case BlockKind.ObjectInitializer:
852-
case BlockKind.CallInlineAssign:
853-
// Allow inlining into the first instruction of the block
854-
if (block.Instructions.Count == 0)
855-
return FindResult.Stop;
856-
return NoContinue(FindLoadInNext(block.Instructions[0], v, expressionBeingMoved, options));
857-
// If FindLoadInNext() returns null, we still can't continue searching
858-
// because we can't inline over the remainder of the block.
859-
case BlockKind.CallWithNamedArgs:
860-
return NamedArgumentTransform.CanExtendNamedArgument(block, v, expressionBeingMoved);
861-
default:
862-
return FindResult.Stop;
832+
// Special case: inlining will change code semantics,
833+
// but we accept that so that we can avoid a ref local.
834+
if (expressionBeingMoved is LdFlda ldflda)
835+
ldflda.DelayExceptions = true;
836+
else if (expressionBeingMoved is LdElema ldelema)
837+
ldelema.DelayExceptions = true;
838+
return FindResult.Found(expr);
863839
}
840+
return FindResult.Stop;
841+
}
842+
else if (expr is Block { Kind: BlockKind.CallWithNamedArgs } block)
843+
{
844+
return NamedArgumentTransform.CanExtendNamedArgument(block, v, expressionBeingMoved);
864845
}
865846
else if (options.HasFlag(InliningOptions.FindDeconstruction) && expr is DeconstructInstruction di)
866847
{
@@ -886,14 +867,6 @@ internal static FindResult FindLoadInNext(ILInstruction expr, ILVariable v, ILIn
886867
return FindResult.Stop; // abort, inlining not possible
887868
}
888869

889-
private static FindResult NoContinue(FindResult findResult)
890-
{
891-
if (findResult.Type == FindResultType.Continue)
892-
return FindResult.Stop;
893-
else
894-
return findResult;
895-
}
896-
897870
/// <summary>
898871
/// Determines whether it is safe to move 'expressionBeingMoved' past 'expr'
899872
/// </summary>

0 commit comments

Comments
 (0)