diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs index ceee27d5e..2f7b344c1 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs @@ -17,10 +17,20 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.Runtime.InteropServices; namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { + public static class DeconstructionExt + { + public static void Deconstruct(this KeyValuePair pair, out K key, out V value) + { + key = pair.Key; + value = pair.Value; + } + } + internal class DeconstructionTests { [StructLayout(LayoutKind.Sequential, Size = 1)] @@ -241,5 +251,19 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { (Get(0).IntField, Get(1).IntField) = GetSource(); } + + public void DeconstructDictionaryForEach(Dictionary dictionary) + { + foreach (var (str, num2) in dictionary) { + Console.WriteLine(str + ": " + num2); + } + } + + public void DeconstructTupleListForEach(List<(string, int)> tuples) + { + foreach (var (str, num) in tuples) { + Console.WriteLine(str + ": " + num); + } + } } } diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 35b9960e7..50295d933 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -1562,7 +1562,8 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor Space(policy.SpacesWithinForeachParentheses); foreachStatement.VariableType.AcceptVisitor(this); Space(); - WriteIdentifier(foreachStatement.VariableNameToken); + foreachStatement.VariableDesignation.AcceptVisitor(this); + Space(); WriteKeyword(ForeachStatement.InKeywordRole); Space(); foreachStatement.InExpression.AcceptVisitor(this); diff --git a/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs b/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs index 7993932c9..072424663 100644 --- a/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs @@ -269,7 +269,7 @@ namespace ICSharpCode.Decompiler.CSharp StartSequencePoint(foreachStatement); AddToSequencePoint(foreachInfo.GetCurrentCall); - EndSequencePoint(foreachStatement.VariableType.StartLocation, foreachStatement.VariableNameToken.EndLocation); + EndSequencePoint(foreachStatement.VariableType.StartLocation, foreachStatement.VariableDesignation.EndLocation); VisitAsSequencePoint(foreachStatement.EmbeddedStatement); } diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index a935bfb99..34109e106 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -495,12 +495,9 @@ namespace ICSharpCode.Decompiler.CSharp if (enumeratorVar2 != enumeratorVar) return null; // Detect which foreach-variable transformation is necessary/possible. - var transformation = DetectGetCurrentTransformation(container, body, enumeratorVar, conditionInst, out var singleGetter, out var foreachVariable); + var transformation = DetectGetCurrentTransformation(container, body, loopContainer, enumeratorVar, conditionInst, out var singleGetter, out var foreachVariable); if (transformation == RequiredGetCurrentTransformation.NoForeach) return null; - // The existing foreach variable, if found, can only be used in the loop container. - if (foreachVariable != null && !(foreachVariable.CaptureScope == null || foreachVariable.CaptureScope == loopContainer)) - return null; // Extract in-expression var collectionExpr = m.Get("collection").Single(); // Special case: foreach (var item in this) is decompiled as foreach (var item in base) @@ -537,6 +534,8 @@ namespace ICSharpCode.Decompiler.CSharp break; } + VariableDesignation designation = null; + // Handle the required foreach-variable transformation: switch (transformation) { case RequiredGetCurrentTransformation.UseExistingVariable: @@ -566,7 +565,18 @@ namespace ICSharpCode.Decompiler.CSharp body.Instructions.Insert(0, new StLoc(localCopyVariable, new LdLoc(foreachVariable))); body.Instructions.Insert(0, new StLoc(foreachVariable, instToReplace)); break; + case RequiredGetCurrentTransformation.Deconstruction: + useVar = true; + designation = TranslateForeachDeconstructionDesignation((DeconstructInstruction)body.Instructions[0]); + break; + } + + if (designation == null) { + designation = new SingleVariableDesignation { Identifier = foreachVariable.Name }; + // Add the variable annotation for highlighting + designation.AddAnnotation(new ILVariableResolveResult(foreachVariable, foreachVariable.Type)); } + // Convert the modified body to C# AST: var whileLoop = (WhileStatement)ConvertAsBlock(container).First(); BlockStatement foreachBody = (BlockStatement)whileLoop.EmbeddedStatement.Detach(); @@ -586,12 +596,10 @@ namespace ICSharpCode.Decompiler.CSharp // Construct the foreach loop. var foreachStmt = new ForeachStatement { VariableType = useVar ? new SimpleType("var") : exprBuilder.ConvertType(foreachVariable.Type), - VariableName = foreachVariable.Name, + VariableDesignation = designation, InExpression = collectionExpr.Detach(), EmbeddedStatement = foreachBody }; - // Add the variable annotation for highlighting (TokenTextWriter expects it directly on the ForeachStatement). - foreachStmt.AddAnnotation(new ILVariableResolveResult(foreachVariable, foreachVariable.Type)); foreachStmt.AddAnnotation(new ForeachAnnotation(inst.ResourceExpression, conditionInst, singleGetter)); foreachStmt.CopyAnnotationsFrom(whileLoop); // If there was an optional return statement, return it as well. @@ -606,6 +614,37 @@ namespace ICSharpCode.Decompiler.CSharp return foreachStmt; } + VariableDesignation TranslateForeachDeconstructionDesignation(DeconstructInstruction inst) + { + var assignments = inst.Assignments.Instructions; + int assignmentPos = 0; + + return ConstructDesignation(inst.Pattern); + + VariableDesignation ConstructDesignation(MatchInstruction matchInstruction) + { + var designations = new ParenthesizedVariableDesignation(); + foreach (var subPattern in matchInstruction.SubPatterns.Cast()) { + if (subPattern.IsVar) { + var designation = new SingleVariableDesignation(); + if (subPattern.HasDesignator) { + ILVariable v = ((StLoc)assignments[assignmentPos]).Variable; + v.Kind = VariableKind.ForeachLocal; + designation.Identifier = v.Name; + designation.AddAnnotation(new ILVariableResolveResult(v)); + assignmentPos++; + } else { + designation.Identifier = "_"; + } + designations.VariableDesignations.Add(designation); + } else { + designations.VariableDesignations.Add(ConstructDesignation(subPattern)); + } + } + return designations; + } + } + static bool EqualErasedType(IType a, IType b) { return NormalizeTypeVisitor.TypeErasure.EquivalentTypes(a, b); @@ -694,7 +733,12 @@ namespace ICSharpCode.Decompiler.CSharp /// ... (ldloca copy) ... /// /// - IntroduceNewVariableAndLocalCopy + IntroduceNewVariableAndLocalCopy, + /// + /// call get_Current() is the tested operand of a deconstruct instruction. + /// and the deconstruct instruction is the first statement in the loop body. + /// + Deconstruction, } /// @@ -707,11 +751,14 @@ namespace ICSharpCode.Decompiler.CSharp /// Returns the call instruction invoking Current's getter. /// Returns the the foreach variable, if a suitable was found. This variable is only assigned once and its assignment is the first statement in . /// for details. - RequiredGetCurrentTransformation DetectGetCurrentTransformation(BlockContainer usingContainer, Block loopBody, ILVariable enumerator, ILInstruction moveNextUsage, out CallInstruction singleGetter, out ILVariable foreachVariable) + RequiredGetCurrentTransformation DetectGetCurrentTransformation(BlockContainer usingContainer, Block loopBody, BlockContainer loopContainer, ILVariable enumerator, ILInstruction moveNextUsage, out CallInstruction singleGetter, out ILVariable foreachVariable) { singleGetter = null; foreachVariable = null; - var loads = (enumerator.LoadInstructions.OfType().Concat(enumerator.AddressInstructions.OfType())).Where(ld => !ld.IsDescendantOf(moveNextUsage)).ToArray(); + var loads = enumerator.LoadInstructions.OfType() + .Concat(enumerator.AddressInstructions.OfType()) + .Where(ld => !ld.IsDescendantOf(moveNextUsage)) + .ToArray(); // enumerator is used in multiple locations or not in conjunction with get_Current // => no foreach if (loads.Length != 1 || !ParentIsCurrentGetter(loads[0])) @@ -719,8 +766,17 @@ namespace ICSharpCode.Decompiler.CSharp singleGetter = (CallInstruction)loads[0].Parent; // singleGetter is not part of the first instruction in body or cannot be uninlined // => no foreach - if (!(singleGetter.IsDescendantOf(loopBody.Instructions[0]) && ILInlining.CanUninline(singleGetter, loopBody.Instructions[0]))) + if (!(singleGetter.IsDescendantOf(loopBody.Instructions[0]) + && ILInlining.CanUninline(singleGetter, loopBody.Instructions[0]))) + { return RequiredGetCurrentTransformation.NoForeach; + } + if (loopBody.Instructions[0] is DeconstructInstruction deconstruction + && singleGetter == deconstruction.Pattern.TestedOperand + && CanBeDeconstructedInForeach(deconstruction, usingContainer, loopContainer)) + { + return RequiredGetCurrentTransformation.Deconstruction; + } ILInstruction inst = singleGetter; // in some cases, i.e. foreach variable with explicit type different from the collection-item-type, // the result of call get_Current is casted. @@ -729,7 +785,7 @@ namespace ICSharpCode.Decompiler.CSharp // One variable was found. if (inst.Parent is StLoc stloc && (stloc.Variable.Kind == VariableKind.Local || stloc.Variable.Kind == VariableKind.StackSlot)) { // Must be a plain assignment expression and variable must only be used in 'body' + only assigned once. - if (stloc.Parent == loopBody && VariableIsOnlyUsedInBlock(stloc, usingContainer)) { + if (stloc.Parent == loopBody && VariableIsOnlyUsedInBlock(stloc, usingContainer, loopContainer)) { foreachVariable = stloc.Variable; return RequiredGetCurrentTransformation.UseExistingVariable; } @@ -743,13 +799,39 @@ namespace ICSharpCode.Decompiler.CSharp return RequiredGetCurrentTransformation.IntroduceNewVariable; } + bool CanBeDeconstructedInForeach(DeconstructInstruction deconstruction, BlockContainer usingContainer, BlockContainer loopContainer) + { + if (deconstruction.Init.Count > 0) + return false; + if (deconstruction.Conversions.Instructions.Count > 0) + return false; + var operandType = deconstruction.Pattern.TestedOperand.InferType(this.typeSystem); + var expectedType = deconstruction.Pattern.Variable.Type; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(operandType, expectedType)) + return false; + foreach (var item in deconstruction.Assignments.Instructions) { + if (!item.MatchStLoc(out var v, out var value)) + return false; + expectedType = ((LdLoc)value).Variable.Type; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(v.Type, expectedType)) + return false; + if (!(v.Kind == VariableKind.StackSlot || v.Kind == VariableKind.Local)) + return false; + if (!VariableIsOnlyUsedInBlock((StLoc)item, usingContainer, loopContainer)) + return false; + if (!(v.CaptureScope == null || v.CaptureScope == usingContainer)) + return false; + } + return true; + } + /// /// Determines whether storeInst.Variable is only assigned once and used only inside . /// Loads by reference (ldloca) are only allowed in the context of this pointer in call instructions, /// or as target of ldobj. /// (This only applies to value types.) /// - bool VariableIsOnlyUsedInBlock(StLoc storeInst, BlockContainer usingContainer) + bool VariableIsOnlyUsedInBlock(StLoc storeInst, BlockContainer usingContainer, BlockContainer loopContainer) { if (storeInst.Variable.LoadInstructions.Any(ld => !ld.IsDescendantOf(usingContainer))) return false; @@ -757,6 +839,8 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (storeInst.Variable.StoreInstructions.OfType().Any(st => st != storeInst)) return false; + if (!(storeInst.Variable.CaptureScope == null || storeInst.Variable.CaptureScope == loopContainer)) + return false; return true; bool AddressUseAllowed(LdLoca la) diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs index c12c82abd..0e8585233 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/DeclarationExpression.cs @@ -31,8 +31,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax } public VariableDesignation Designation { - get { return GetChildByRole(VariableDesignation.VariableDesignationRole); } - set { SetChildByRole(VariableDesignation.VariableDesignationRole, value); } + get { return GetChildByRole(Roles.VariableDesignationRole); } + set { SetChildByRole(Roles.VariableDesignationRole, value); } } public override void AcceptVisitor(IAstVisitor visitor) diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs index 872f977aa..b45935814 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs @@ -46,11 +46,12 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public static readonly Role Variable = new Role ("Variable", VariableInitializer.Null); public static readonly Role EmbeddedStatement = new Role ("EmbeddedStatement", Statement.Null); public readonly static Role TypeMemberRole = new Role ("TypeMember"); - + + public static readonly Role VariableDesignationRole = new Role("VariableDesignation", VariableDesignation.Null); // public static readonly TokenRole Keyword = new TokenRole ("Keyword", CSharpTokenNode.Null); -// public static readonly TokenRole InKeyword = new TokenRole ("InKeyword", CSharpTokenNode.Null); - + // public static readonly TokenRole InKeyword = new TokenRole ("InKeyword", CSharpTokenNode.Null); + // some pre defined constants for most used punctuation public static readonly TokenRole LPar = new TokenRole ("("); public static readonly TokenRole RPar = new TokenRole (")"); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs index 4c1d037c2..d2bd3f3d3 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs @@ -47,23 +47,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax get { return GetChildByRole (Roles.Type); } set { SetChildByRole (Roles.Type, value); } } - - public string VariableName { - get { - return GetChildByRole (Roles.Identifier).Name; - } - set { - SetChildByRole(Roles.Identifier, Identifier.Create (value)); - } - } - - public Identifier VariableNameToken { - get { - return GetChildByRole (Roles.Identifier); - } - set { - SetChildByRole(Roles.Identifier, value); - } + + public VariableDesignation VariableDesignation { + get { return GetChildByRole(Roles.VariableDesignationRole); } + set { SetChildByRole(Roles.VariableDesignationRole, value); } } public CSharpTokenNode InToken { @@ -102,7 +89,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax protected internal override bool DoMatch(AstNode other, PatternMatching.Match match) { ForeachStatement o = other as ForeachStatement; - return o != null && this.VariableType.DoMatch(o.VariableType, match) && MatchString(this.VariableName, o.VariableName) + return o != null && this.VariableType.DoMatch(o.VariableType, match) && this.VariableDesignation.DoMatch(o.VariableDesignation, match) && this.InExpression.DoMatch(o.InExpression, match) && this.EmbeddedStatement.DoMatch(o.EmbeddedStatement, match); } } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/VariableDesignation.cs b/ICSharpCode.Decompiler/CSharp/Syntax/VariableDesignation.cs index d79ebdf65..8d7991dec 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/VariableDesignation.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/VariableDesignation.cs @@ -16,19 +16,47 @@ // 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.Security.Cryptography.X509Certificates; -using System.Text; - using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching; -using ICSharpCode.Decompiler.DebugInfo; namespace ICSharpCode.Decompiler.CSharp.Syntax { public abstract class VariableDesignation : AstNode { - public static Role VariableDesignationRole = new Role("VariableDesignation"); + public override NodeType NodeType => NodeType.Unknown; + + #region Null + public new static readonly VariableDesignation Null = new NullVariableDesignation(); + + sealed class NullVariableDesignation : VariableDesignation + { + public override bool IsNull { + get { + return true; + } + } + + public override void AcceptVisitor(IAstVisitor visitor) + { + visitor.VisitNullNode(this); + } + + public override T AcceptVisitor(IAstVisitor visitor) + { + return visitor.VisitNullNode(this); + } + + public override S AcceptVisitor(IAstVisitor visitor, T data) + { + return visitor.VisitNullNode(this, data); + } + + protected internal override bool DoMatch(AstNode other, PatternMatching.Match match) + { + return other == null || other.IsNull; + } + } + #endregion + } /// @@ -36,7 +64,6 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax /// public class SingleVariableDesignation : VariableDesignation { - public override NodeType NodeType => NodeType.Unknown; public string Identifier { get { return GetChildByRole(Roles.Identifier).Name; } @@ -80,15 +107,13 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax } public AstNodeCollection VariableDesignations { - get { return GetChildrenByRole(VariableDesignation.VariableDesignationRole); } + get { return GetChildrenByRole(Roles.VariableDesignationRole); } } public CSharpTokenNode RParToken { get { return GetChildByRole(Roles.RPar); } } - public override NodeType NodeType => NodeType.Unknown; - public override void AcceptVisitor(IAstVisitor visitor) { visitor.VisitParenthesizedVariableDesignation(this); diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs index 899934023..c65d02d75 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs @@ -337,14 +337,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms body.Statements.Add(statement.Detach()); var foreachStmt = new ForeachStatement { VariableType = context.Settings.AnonymousTypes && itemVariable.Type.ContainsAnonymousType() ? new SimpleType("var") : context.TypeSystemAstBuilder.ConvertType(itemVariable.Type), - VariableName = itemVariable.Name, + VariableDesignation = new SingleVariableDesignation { Identifier = itemVariable.Name }, InExpression = m.Get("arrayVariable").Single().Detach(), EmbeddedStatement = body }; foreachStmt.CopyAnnotationsFrom(forStatement); itemVariable.Kind = IL.VariableKind.ForeachLocal; // Add the variable annotation for highlighting (TokenTextWriter expects it directly on the ForeachStatement). - foreachStmt.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); + foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); // TODO : add ForeachAnnotation forStatement.ReplaceWith(foreachStmt); return foreachStmt; @@ -495,7 +495,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms body.Statements.Add(statement.Detach()); var foreachStmt = new ForeachStatement { VariableType = context.Settings.AnonymousTypes && itemVariable.Type.ContainsAnonymousType() ? new SimpleType("var") : context.TypeSystemAstBuilder.ConvertType(itemVariable.Type), - VariableName = itemVariable.Name, + VariableDesignation = new SingleVariableDesignation { Identifier = itemVariable.Name }, InExpression = m.Get("collection").Single().Detach(), EmbeddedStatement = body }; @@ -504,7 +504,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms //foreachStmt.CopyAnnotationsFrom(forStatement); itemVariable.Kind = IL.VariableKind.ForeachLocal; // Add the variable annotation for highlighting (TokenTextWriter expects it directly on the ForeachStatement). - foreachStmt.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); + foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); // TODO : add ForeachAnnotation expressionStatement.ReplaceWith(foreachStmt); return foreachStmt; diff --git a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs index 82730d64f..ce2f7c49b 100644 --- a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs +++ b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs @@ -164,7 +164,7 @@ namespace ICSharpCode.Decompiler if (node is Identifier && node.Parent != null) node = node.Parent; - if (node is ParameterDeclaration || node is VariableInitializer || node is CatchClause || node is ForeachStatement) { + if (node is ParameterDeclaration || node is VariableInitializer || node is CatchClause || node is VariableDesignation) { var variable = node.Annotation()?.Variable; if (variable != null) return variable;