Skip to content

Commit 62fcab8

Browse files
committed
#2050: Allow inlining into the StObj target slot when this is possible without changing the program semantics.
1 parent 761c3fe commit 62fcab8

File tree

8 files changed

+66
-15
lines changed

8 files changed

+66
-15
lines changed

ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public void Loops([ValueSource("defaultOptions")] CompilerOptions options)
139139
[Test]
140140
public void NullableTests([ValueSource("defaultOptions")] CompilerOptions options)
141141
{
142-
RunCS(options: options);
142+
RunCS(options: options, forceRoslynRecompile: true);
143143
}
144144

145145
[Test]
@@ -301,7 +301,7 @@ public void MiniJSON([ValueSource("defaultOptions")] CompilerOptions options)
301301
RunCS(options: options);
302302
}
303303

304-
void RunCS([CallerMemberName] string testName = null, CompilerOptions options = CompilerOptions.UseDebug)
304+
void RunCS([CallerMemberName] string testName = null, CompilerOptions options = CompilerOptions.UseDebug, bool forceRoslynRecompile = false)
305305
{
306306
string testFileName = testName + ".cs";
307307
string testOutputFileName = testName + Tester.GetSuffix(options) + ".exe";
@@ -311,7 +311,7 @@ void RunCS([CallerMemberName] string testName = null, CompilerOptions options =
311311
outputFile = Tester.CompileCSharp(Path.Combine(TestCasePath, testFileName), options,
312312
outputFileName: Path.Combine(TestCasePath, testOutputFileName));
313313
string decompiledCodeFile = Tester.DecompileCSharp(outputFile.PathToAssembly, Tester.GetSettings(options));
314-
if (options.HasFlag(CompilerOptions.UseMcs)) {
314+
if (forceRoslynRecompile || options.HasFlag(CompilerOptions.UseMcs)) {
315315
// For second pass, use roslyn instead of mcs.
316316
// mcs has some compiler bugs that cause it to not accept ILSpy-generated code,
317317
// for example when there's unreachable code due to other compiler bugs in the first mcs run.

ICSharpCode.Decompiler.Tests/TestCases/Correctness/NullableTests.cs

+46-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ static void Main()
2727
AvoidLifting();
2828
BitNot();
2929
FieldAccessOrderOfEvaluation(null);
30+
ArrayAccessOrderOfEvaluation();
3031
}
3132

3233
static void AvoidLifting()
@@ -85,10 +86,10 @@ static void Assert(bool b)
8586
}
8687

8788

88-
static int GetInt()
89+
static T GetValue<T>()
8990
{
90-
Console.WriteLine("got int");
91-
return 42;
91+
Console.WriteLine("GetValue");
92+
return default(T);
9293
}
9394

9495
int intField;
@@ -97,19 +98,59 @@ static void FieldAccessOrderOfEvaluation(NullableTests c)
9798
{
9899
Console.WriteLine("GetInt, then NRE:");
99100
try {
100-
c.intField = GetInt();
101+
c.intField = GetValue<int>();
101102
} catch (Exception ex) {
102103
Console.WriteLine(ex.Message);
103104
}
104105
Console.WriteLine("NRE before GetInt:");
105106
try {
106107
#if CS60
107108
ref int i = ref c.intField;
108-
i = GetInt();
109+
i = GetValue<int>();
109110
#endif
110111
} catch (Exception ex) {
111112
Console.WriteLine(ex.Message);
112113
}
113114
}
115+
116+
static T[] GetArray<T>()
117+
{
118+
Console.WriteLine("GetArray");
119+
return null;
120+
}
121+
122+
static int GetIndex()
123+
{
124+
Console.WriteLine("GetIndex");
125+
return 0;
126+
}
127+
128+
static void ArrayAccessOrderOfEvaluation()
129+
{
130+
Console.WriteLine("GetArray direct:");
131+
try {
132+
GetArray<int>()[GetIndex()] = GetValue<int>();
133+
} catch (Exception ex) {
134+
Console.WriteLine(ex.Message);
135+
}
136+
Console.WriteLine("GetArray with ref:");
137+
try {
138+
#if CS60
139+
ref int elem = ref GetArray<int>()[GetIndex()];
140+
elem = GetValue<int>();
141+
#endif
142+
} catch (Exception ex) {
143+
Console.WriteLine(ex.Message);
144+
}
145+
Console.WriteLine("GetArray direct with value-type:");
146+
try {
147+
// This line is mis-compiled by legacy csc:
148+
// with the legacy compiler the NRE is thrown before the GetValue call;
149+
// with Roslyn the NRE is thrown after the GetValue call.
150+
GetArray<TimeSpan>()[GetIndex()] = GetValue<TimeSpan>();
151+
} catch (Exception ex) {
152+
Console.WriteLine(ex.Message);
153+
}
154+
}
114155
}
115156
}

ICSharpCode.Decompiler/IL/Instructions.cs

-1
Original file line numberDiff line numberDiff line change
@@ -4117,7 +4117,6 @@ internal override void CheckInvariant(ILPhase phase)
41174117
base.CheckInvariant(phase);
41184118
Debug.Assert(target.ResultType == StackType.Ref || target.ResultType == StackType.I);
41194119
Debug.Assert(value.ResultType == type.GetStackType());
4120-
Debug.Assert(IsValidTarget(target));
41214120
}
41224121
}
41234122
}

ICSharpCode.Decompiler/IL/Instructions.tt

+1-2
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,7 @@
253253
new OpCode("stobj", "Indirect store (store to ref/pointer)." + Environment.NewLine
254254
+ "Evaluates to the value that was stored (when using type byte/short: evaluates to the truncated value, sign/zero extended back to I4 based on type.GetSign())",
255255
CustomClassName("StObj"), CustomArguments(("target", new[] { "Ref", "I" }), ("value", new[] { "type.GetStackType()" })), HasTypeOperand, MemoryAccess, CustomWriteToButKeepOriginal,
256-
SupportsVolatilePrefix, SupportsUnalignedPrefix, MayThrow, ResultType("type.GetStackType()"),
257-
CustomInvariant("Debug.Assert(IsValidTarget(target));")),
256+
SupportsVolatilePrefix, SupportsUnalignedPrefix, MayThrow, ResultType("type.GetStackType()")),
258257

259258
new OpCode("box", "Boxes a value.",
260259
Unary, HasTypeOperand, MemoryAccess, MayThrow, ResultType("O")),

ICSharpCode.Decompiler/IL/Instructions/LdFlda.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,18 @@ public sealed partial class StObj
5252
/// This means a LdFlda/LdElema used as target for StObj must have DelayExceptions==true to allow a translation to C#
5353
/// without changing the program semantics. See https://github.com/icsharpcode/ILSpy/issues/2050
5454
/// </summary>
55-
/// <remarks>This is part of the StObj invariant.</remarks>
56-
public static bool IsValidTarget(ILInstruction inst)
55+
public bool CanInlineIntoTargetSlot(ILInstruction inst)
5756
{
5857
switch (inst.OpCode) {
5958
case OpCode.LdElema:
6059
case OpCode.LdFlda:
61-
return !inst.HasDirectFlag(InstructionFlags.MayThrow);
60+
Debug.Assert(inst.HasDirectFlag(InstructionFlags.MayThrow));
61+
// If the ldelema/ldflda may throw a non-delayed exception, inlining will cause it
62+
// to turn into a delayed exception after the translation to C#.
63+
// This is only valid if the value computation doesn't involve any side effects.
64+
return SemanticHelper.IsPure(this.Value.Flags);
65+
// Note that after inlining such a ldelema/ldflda, the normal inlining rules will
66+
// prevent us from inlining an effectful instruction into the value slot.
6267
default:
6368
return true;
6469
}

ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ internal static FindResult FindLoadInNext(ILInstruction expr, ILVariable v, ILIn
581581
return FindResult.Stop;
582582
if (expr.MatchLdLoc(v) || expr.MatchLdLoca(v)) {
583583
// Match found, we can inline
584-
if (expr.SlotInfo == StObj.TargetSlot && !StObj.IsValidTarget(expressionBeingMoved)) {
584+
if (expr.SlotInfo == StObj.TargetSlot && !((StObj)expr.Parent).CanInlineIntoTargetSlot(expressionBeingMoved)) {
585585
// special case: the StObj.TargetSlot does not accept some kinds of expressions
586586
return FindResult.Stop;
587587
}

ICSharpCode.Decompiler/IL/Transforms/StatementTransform.cs

+5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public interface IStatementTransform
4545
/// <remarks>
4646
/// Instructions prior to block.Instructions[pos] must not be modified.
4747
/// It is valid to read such instructions, but not recommended as those have not been transformed yet.
48+
///
49+
/// This function is only called on control-flow blocks with unreachable end-point.
50+
/// Thus, the last instruction in the block always must have the EndPointUnreachable flag.
51+
/// ==> Instructions with reachable end can't be last. Some transforms use this to save some bounds checks.
4852
/// </remarks>
4953
void Run(Block block, int pos, StatementTransformContext context);
5054
}
@@ -122,6 +126,7 @@ public void Run(Block block, BlockTransformContext context)
122126
ctx.rerunPosition = null;
123127
}
124128
foreach (var transform in children) {
129+
Debug.Assert(block.HasFlag(InstructionFlags.EndPointUnreachable));
125130
transform.Run(block, pos, ctx);
126131
#if DEBUG
127132
block.Instructions[pos].CheckInvariant(ILPhase.Normal);

ILSpy/TextView/ILAsm-Mode.xshd

+2
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
<Word>newarr</Word>
150150
<Word>ldlen</Word>
151151
<Word>ldelema</Word>
152+
<Word>ldelem</Word>
152153
<Word>ldelem.i1</Word>
153154
<Word>ldelem.u1</Word>
154155
<Word>ldelem.i2</Word>
@@ -160,6 +161,7 @@
160161
<Word>ldelem.r4</Word>
161162
<Word>ldelem.r8</Word>
162163
<Word>ldelem.ref</Word>
164+
<Word>stelem</Word>
163165
<Word>stelem.i</Word>
164166
<Word>stelem.i1</Word>
165167
<Word>stelem.i2</Word>

0 commit comments

Comments
 (0)