// Copyright (c) 2020 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; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Resources; using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; namespace ICSharpCode.Decompiler.IL.Transforms { /// /// /// class DeconstructionTransform : IStatementTransform { StatementTransformContext context; readonly Dictionary deconstructionResultsLookup = new Dictionary(); ILVariable[] deconstructionResults; ILVariable tupleVariable; TupleType tupleType; /* stloc tuple(call MakeIntIntTuple(ldloc this)) ---- stloc myInt(call op_Implicit(ldfld Item2(ldloca tuple))) stloc a(ldfld Item1(ldloca tuple)) stloc b(ldloc myInt) ==> deconstruct { init: deconstruct: match.deconstruct(temp = ldloca tuple) { match(result0 = deconstruct.result 0(temp)), match(result1 = deconstruct.result 1(temp)) } conversions: { stloc conv2(call op_Implicit(ldloc result1)) } assignments: { stloc a(ldloc result0) stloc b(ldloc conv2) } } * */ void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) { if (!context.Settings.Deconstruction) return; try { this.context = context; Reset(); if (TransformDeconstruction(block, pos)) return; if (InlineDeconstructionInitializer(block, pos)) return; } finally { this.context = null; Reset(); } } private void Reset() { this.deconstructionResultsLookup.Clear(); this.tupleVariable = null; this.tupleType = null; this.deconstructionResults = null; } struct ConversionInfo { public IType inputType; public Conv conv; } /// /// Get index of deconstruction result or tuple element /// Returns -1 on failure. /// int FindIndex(ILInstruction inst, out Action delayedActions) { delayedActions = null; if (inst.MatchLdLoc(out var v)) { if (!deconstructionResultsLookup.TryGetValue(v, out int index)) return -1; return index; } if (inst.MatchLdFld(out _, out _)) { if (!TupleTransform.MatchTupleFieldAccess((LdFlda)((LdObj)inst).Target, out var tupleType, out var target, out int index)) return -1; // Item fields are one-based, we use zero-based indexing. index--; // normalize tuple type tupleType = TupleType.FromUnderlyingType(context.TypeSystem, tupleType); if (!target.MatchLdLoca(out v)) return -1; if (this.tupleVariable == null) { this.tupleVariable = v; this.tupleType = (TupleType)tupleType; this.deconstructionResults = new ILVariable[this.tupleType.Cardinality]; } if (this.tupleType.Cardinality < 2) return -1; if (v != tupleVariable || !this.tupleType.Equals(tupleType)) return -1; if (this.deconstructionResults[index] == null) { var freshVar = new ILVariable(VariableKind.StackSlot, this.tupleType.ElementTypes[index]) { Name = "E_" + index }; delayedActions += _ => context.Function.Variables.Add(freshVar); this.deconstructionResults[index] = freshVar; } delayedActions += _ => { inst.ReplaceWith(new LdLoc(this.deconstructionResults[index])); }; return index; } return -1; } /// /// stloc v(value) /// expr(..., deconstruct { ... }, ...) /// => /// expr(..., deconstruct { init: stloc v(value) ... }, ...) /// bool InlineDeconstructionInitializer(Block block, int pos) { if (!block.Instructions[pos].MatchStLoc(out var v, out var value)) return false; if (!(v.IsSingleDefinition && v.LoadCount == 1)) return false; if (pos + 1 >= block.Instructions.Count) return false; var result = ILInlining.FindLoadInNext(block.Instructions[pos + 1], v, value, InliningOptions.FindDeconstruction); if (result.Type != ILInlining.FindResultType.Deconstruction) return false; var deconstruction = (DeconstructInstruction)result.LoadInst; LdLoc loadInst = v.LoadInstructions[0]; if (!loadInst.IsDescendantOf(deconstruction.Assignments)) return false; if (loadInst.SlotInfo == StObj.TargetSlot) { if (value.OpCode == OpCode.LdFlda || value.OpCode == OpCode.LdElema) return false; } if (deconstruction.Init.Count > 0) { var a = deconstruction.Init[0].Variable.LoadInstructions.Single(); var b = v.LoadInstructions.Single(); if (!b.IsBefore(a)) return false; } context.Step("InlineDeconstructionInitializer", block.Instructions[pos]); deconstruction.Init.Insert(0, (StLoc)block.Instructions[pos]); block.Instructions.RemoveAt(pos); v.Kind = VariableKind.DeconstructionInitTemporary; return true; } bool TransformDeconstruction(Block block, int pos) { int startPos = pos; Action delayedActions = null; if (MatchDeconstruction(block.Instructions[pos], out IMethod deconstructMethod, out ILInstruction rootTestedOperand)) { pos++; } if (!MatchConversions(block, ref pos, out var conversions, out var conversionStLocs, ref delayedActions)) return false; if (!MatchAssignments(block, ref pos, conversions, conversionStLocs, ref delayedActions)) return false; // first tuple element may not be discarded, // otherwise we would run this transform on a suffix of the actual pattern. if (deconstructionResults[0] == null) return false; context.Step("Deconstruction", block.Instructions[startPos]); DeconstructInstruction replacement = new DeconstructInstruction(); IType deconstructedType; if (deconstructMethod == null) { deconstructedType = this.tupleType; rootTestedOperand = new LdLoc(this.tupleVariable); } else { if (deconstructMethod.IsStatic) { deconstructedType = deconstructMethod.Parameters[0].Type; } else { deconstructedType = deconstructMethod.DeclaringType; } } var rootTempVariable = context.Function.RegisterVariable(VariableKind.PatternLocal, deconstructedType); replacement.Pattern = new MatchInstruction(rootTempVariable, deconstructMethod, rootTestedOperand) { IsDeconstructCall = deconstructMethod != null, IsDeconstructTuple = this.tupleType != null }; int index = 0; foreach (ILVariable v in deconstructionResults) { var result = v; if (result == null) { var freshVar = new ILVariable(VariableKind.PatternLocal, this.tupleType.ElementTypes[index]) { Name = "E_" + index }; context.Function.Variables.Add(freshVar); result = freshVar; } else { result.Kind = VariableKind.PatternLocal; } replacement.Pattern.SubPatterns.Add( new MatchInstruction( result, new DeconstructResultInstruction(index, result.StackType, new LdLoc(rootTempVariable)) ) ); index++; } replacement.Conversions = new Block(BlockKind.DeconstructionConversions); foreach (var convInst in conversionStLocs) { replacement.Conversions.Instructions.Add(convInst); } replacement.Assignments = new Block(BlockKind.DeconstructionAssignments); delayedActions?.Invoke(replacement); block.Instructions[startPos] = replacement; block.Instructions.RemoveRange(startPos + 1, pos - startPos - 1); return true; } bool MatchDeconstruction(ILInstruction inst, out IMethod deconstructMethod, out ILInstruction testedOperand) { testedOperand = null; deconstructMethod = null; deconstructionResults = null; if (!(inst is CallInstruction call)) return false; if (!MatchInstruction.IsDeconstructMethod(call.Method)) return false; if (call.Method.IsStatic == call is CallVirt) return false; if (call.Arguments.Count < 3) return false; deconstructionResults = new ILVariable[call.Arguments.Count - 1]; for (int i = 0; i < deconstructionResults.Length; i++) { if (!call.Arguments[i + 1].MatchLdLoca(out var v)) return false; // TODO v.LoadCount may be 2 if the deconstruction is assigned to a tuple variable // or 0? because of discards if (!(v.StoreCount == 0 && v.AddressCount == 1 && v.LoadCount <= 1)) return false; deconstructionResultsLookup.Add(v, i); deconstructionResults[i] = v; } testedOperand = call.Arguments[0]; deconstructMethod = call.Method; return true; } bool MatchConversions(Block block, ref int pos, out Dictionary conversions, out List conversionStLocs, ref Action delayedActions) { conversions = new Dictionary(); conversionStLocs = new List(); int previousIndex = -1; while (MatchConversion( block.Instructions.ElementAtOrDefault(pos), out var inputInstruction, out var outputVariable, out var info)) { int index = FindIndex(inputInstruction, out var tupleAccessAdjustment); if (index <= previousIndex) return false; if (!(outputVariable.IsSingleDefinition && outputVariable.LoadCount == 1)) return false; delayedActions += tupleAccessAdjustment; deconstructionResultsLookup.Add(outputVariable, index); conversions.Add(outputVariable, info); conversionStLocs.Add((StLoc)block.Instructions[pos]); pos++; previousIndex = index; } return true; } bool MatchConversion(ILInstruction inst, out ILInstruction inputInstruction, out ILVariable outputVariable, out ConversionInfo info) { info = default; inputInstruction = null; if (!inst.MatchStLoc(out outputVariable, out var value)) return false; if (!(value is Conv conv)) return false; info = new ConversionInfo { inputType = conv.Argument.InferType(context.TypeSystem), conv = conv }; inputInstruction = conv.Argument; return true; } bool MatchAssignments(Block block, ref int pos, Dictionary conversions, List conversionStLocs, ref Action delayedActions) { int previousIndex = -1; int conversionStLocIndex = 0; int startPos = pos; while (MatchAssignment(block.Instructions.ElementAtOrDefault(pos), out var targetType, out var valueInst, out var addAssignment)) { int index = FindIndex(valueInst, out var tupleAccessAdjustment); if (index <= previousIndex) return false; AddMissingAssignmentsForConversions(index, ref delayedActions); if (!(valueInst.MatchLdLoc(out var resultVariable) && conversions.TryGetValue(resultVariable, out var conversionInfo))) { conversionInfo = new ConversionInfo { inputType = valueInst.InferType(context.TypeSystem) }; } if (block.Instructions[pos].MatchStLoc(out var assignmentTarget, out _) && assignmentTarget.Kind == VariableKind.StackSlot && assignmentTarget.IsSingleDefinition && conversionInfo.conv == null) { delayedActions += _ => { assignmentTarget.Type = conversionInfo.inputType; }; } else { if (!IsCompatibleImplicitConversion(targetType, conversionInfo)) return false; } delayedActions += addAssignment; delayedActions += tupleAccessAdjustment; pos++; previousIndex = index; } AddMissingAssignmentsForConversions(int.MaxValue, ref delayedActions); return startPos != pos; void AddMissingAssignmentsForConversions(int index, ref Action delayedActions) { while (conversionStLocIndex < conversionStLocs.Count) { var stLoc = conversionStLocs[conversionStLocIndex]; int conversionResultIndex = deconstructionResultsLookup[stLoc.Variable]; if (conversionResultIndex >= index) break; if (conversionResultIndex > previousIndex) { delayedActions += (DeconstructInstruction deconstructInst) => { var freshVar = context.Function.RegisterVariable(VariableKind.StackSlot, stLoc.Variable.Type); deconstructInst.Assignments.Instructions.Add(new StLoc(stLoc.Variable, new LdLoc(freshVar))); stLoc.Variable = freshVar; }; } conversionStLocIndex++; } } } bool MatchAssignment(ILInstruction inst, out IType targetType, out ILInstruction valueInst, out Action addAssignment) { targetType = null; valueInst = null; addAssignment = null; if (inst == null) return false; if (inst.MatchStLoc(out var v, out var value) && value is Block block && block.MatchInlineAssignBlock(out var call, out valueInst)) { if (!DeconstructInstruction.IsAssignment(call, context.TypeSystem, out targetType, out _)) return false; if (!(v.IsSingleDefinition && v.LoadCount == 0)) return false; var valueInstCopy = valueInst; addAssignment = (DeconstructInstruction deconstructInst) => { call.Arguments[call.Arguments.Count - 1] = valueInstCopy; deconstructInst.Assignments.Instructions.Add(call); }; return true; } else if (DeconstructInstruction.IsAssignment(inst, context.TypeSystem, out targetType, out valueInst)) { // OK - use the assignment as is addAssignment = (DeconstructInstruction deconstructInst) => { deconstructInst.Assignments.Instructions.Add(inst); }; return true; } else { return false; } } bool IsCompatibleImplicitConversion(IType targetType, ConversionInfo conversionInfo) { var c = CSharpConversions.Get(context.TypeSystem) .ImplicitConversion(conversionInfo.inputType, targetType); if (!c.IsValid) return false; var inputType = conversionInfo.inputType; var conv = conversionInfo.conv; if (c.IsIdentityConversion || c.IsReferenceConversion) { return conv == null || conv.Kind == ConversionKind.Nop; } if (c.IsNumericConversion) { switch (conv.Kind) { case ConversionKind.IntToFloat: return inputType.GetSign() == conv.InputSign; case ConversionKind.FloatPrecisionChange: return true; case ConversionKind.SignExtend: return inputType.GetSign() == Sign.Signed; case ConversionKind.ZeroExtend: return inputType.GetSign() == Sign.Unsigned; default: return false; } } return false; } } }