// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team // // 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.Diagnostics; using System.Linq; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; namespace ICSharpCode.Decompiler.CSharp.Transforms { /// /// Insert variable declarations. /// public class DeclareVariables : IAstTransform { /// /// Represents a position immediately before nextNode. /// nextNode is either an ExpressionStatement in a BlockStatement, or an initializer in a for-loop. /// [DebuggerDisplay("level = {level}, nextNode = {nextNode}")] struct InsertionPoint { /// /// The nesting level of `nextNode` within the AST. /// Used to speed up FindCommonParent(). /// internal int level; internal AstNode nextNode; /// Go up one level internal InsertionPoint Up() { return new InsertionPoint { level = level - 1, nextNode = nextNode.Parent }; } internal InsertionPoint UpTo(int targetLevel) { InsertionPoint result = this; while (result.level > targetLevel) { result.nextNode = result.nextNode.Parent; result.level -= 1; } return result; } } [DebuggerDisplay("VariableToDeclare(Name={Name})")] class VariableToDeclare { public readonly ILVariable ILVariable; public IType Type => ILVariable.Type; public string Name => ILVariable.Name; /// /// Whether the variable needs to be default-initialized. /// public bool DefaultInitialization; /// /// Integer value that can be used to compare to VariableToDeclare instances /// to determine which variable was used first in the source code. /// /// The variable with the lower SourceOrder value has the insertion point /// that comes first in the source code. /// public int SourceOrder; /// /// The insertion point, i.e. the node before which the variable declaration should be inserted. /// public InsertionPoint InsertionPoint; /// /// The first use of the variable. /// public IdentifierExpression FirstUse; public VariableToDeclare ReplacementDueToCollision; public bool InvolvedInCollision; public bool RemovedDueToCollision => ReplacementDueToCollision != null; public bool DeclaredInDeconstruction; public VariableToDeclare(ILVariable variable, bool defaultInitialization, InsertionPoint insertionPoint, IdentifierExpression firstUse, int sourceOrder) { this.ILVariable = variable; this.DefaultInitialization = defaultInitialization; this.InsertionPoint = insertionPoint; this.FirstUse = firstUse; this.SourceOrder = sourceOrder; } } readonly Dictionary variableDict = new Dictionary(); TransformContext context; public void Run(AstNode rootNode, TransformContext context) { try { if (this.context != null) throw new InvalidOperationException("Reentrancy in DeclareVariables?"); this.context = context; variableDict.Clear(); EnsureExpressionStatementsAreValid(rootNode); FindInsertionPoints(rootNode, 0); ResolveCollisions(); InsertDeconstructionVariableDeclarations(); InsertVariableDeclarations(context); UpdateAnnotations(rootNode); } finally { this.context = null; variableDict.Clear(); } } /// /// Analyze the input AST (containing undeclared variables) /// for where those variables would be declared by this transform. /// Analysis does not modify the AST. /// public void Analyze(AstNode rootNode) { variableDict.Clear(); FindInsertionPoints(rootNode, 0); ResolveCollisions(); } /// /// Get the position where the declaration for the variable will be inserted. /// public AstNode GetDeclarationPoint(ILVariable variable) { VariableToDeclare v = variableDict[variable]; while (v.ReplacementDueToCollision != null) { v = v.ReplacementDueToCollision; } return v.InsertionPoint.nextNode; } /// /// Determines whether a variable was merged with other variables. /// public bool WasMerged(ILVariable variable) { VariableToDeclare v = variableDict[variable]; return v.InvolvedInCollision || v.RemovedDueToCollision; } public void ClearAnalysisResults() { variableDict.Clear(); } #region EnsureExpressionStatementsAreValid void EnsureExpressionStatementsAreValid(AstNode rootNode) { foreach (var stmt in rootNode.DescendantsAndSelf.OfType()) { if (!IsValidInStatementExpression(stmt.Expression)) { // fetch ILFunction var function = stmt.Ancestors.SelectMany(a => a.Annotations.OfType()).First(f => f.Parent == null); // if possible use C# 7.0 discard-assignment if (context.Settings.Discards && !ExpressionBuilder.HidesVariableWithName(function, "_")) { stmt.Expression = new AssignmentExpression( new IdentifierExpression("_"), // no ResolveResult stmt.Expression.Detach()); } else { // assign result to dummy variable var type = stmt.Expression.GetResolveResult().Type; var v = function.RegisterVariable( VariableKind.StackSlot, type, AssignVariableNames.GenerateVariableName(function, type, stmt.Expression.Annotations.OfType().Where(AssignVariableNames.IsSupportedInstruction).FirstOrDefault()) ); stmt.Expression = new AssignmentExpression( new IdentifierExpression(v.Name).WithRR(new ILVariableResolveResult(v, v.Type)), stmt.Expression.Detach()); } } } } private static bool IsValidInStatementExpression(Expression expr) { switch (expr) { case InvocationExpression _: case ObjectCreateExpression _: case AssignmentExpression _: case ErrorExpression _: return true; case UnaryOperatorExpression uoe: switch (uoe.Operator) { case UnaryOperatorType.PostIncrement: case UnaryOperatorType.PostDecrement: case UnaryOperatorType.Increment: case UnaryOperatorType.Decrement: case UnaryOperatorType.Await: return true; case UnaryOperatorType.NullConditionalRewrap: return IsValidInStatementExpression(uoe.Expression); default: return false; } default: return false; } } #endregion #region FindInsertionPoints List<(InsertionPoint InsertionPoint, BlockContainer Scope)> scopeTracking = new List<(InsertionPoint, BlockContainer)>(); /// /// Finds insertion points for all variables used within `node` /// and adds them to the variableDict. /// /// `level` == nesting depth of `node` within root node. /// /// /// Insertion point for a variable = common parent of all uses of that variable /// = smallest possible scope that contains all the uses of the variable /// void FindInsertionPoints(AstNode node, int nodeLevel) { BlockContainer scope = node.Annotation(); if (scope != null && (scope.EntryPoint.IncomingEdgeCount > 1 || scope.Parent is ILFunction)) { // track loops and function bodies as scopes, for comparison with CaptureScope. scopeTracking.Add((new InsertionPoint { level = nodeLevel, nextNode = node }, scope)); } else { scope = null; // don't remove a scope if we didn't add one } try { for (AstNode child = node.FirstChild; child != null; child = child.NextSibling) { FindInsertionPoints(child, nodeLevel + 1); } if (node is IdentifierExpression identExpr) { var rr = identExpr.GetResolveResult() as ILVariableResolveResult; if (rr != null && VariableNeedsDeclaration(rr.Variable.Kind)) { FindInsertionPointForVariable(rr.Variable); } else if (identExpr.Annotation() is ILFunction localFunction && localFunction.Kind == ILFunctionKind.LocalFunction) { foreach (var v in localFunction.CapturedVariables) { if (VariableNeedsDeclaration(v.Kind)) FindInsertionPointForVariable(v); } } void FindInsertionPointForVariable(ILVariable variable) { InsertionPoint newPoint; int startIndex = scopeTracking.Count - 1; if (variable.CaptureScope != null && startIndex > 0 && variable.CaptureScope != scopeTracking[startIndex].Scope) { while (startIndex > 0 && scopeTracking[startIndex].Scope != variable.CaptureScope) startIndex--; newPoint = scopeTracking[startIndex + 1].InsertionPoint; } else { newPoint = new InsertionPoint { level = nodeLevel, nextNode = identExpr }; if (variable.HasInitialValue) { // Uninitialized variables are logically initialized at the beginning of the function // Because it's possible that the variable has a loop-carried dependency, // declare it outside of any loops. while (startIndex >= 0) { if (scopeTracking[startIndex].Scope.EntryPoint.IncomingEdgeCount > 1) { // declare variable outside of loop newPoint = scopeTracking[startIndex].InsertionPoint; } else if (scopeTracking[startIndex].Scope.Parent is ILFunction) { // stop at beginning of function break; } startIndex--; } } } if (variableDict.TryGetValue(variable, out VariableToDeclare v)) { v.InsertionPoint = FindCommonParent(v.InsertionPoint, newPoint); } else { v = new VariableToDeclare(variable, variable.HasInitialValue, newPoint, identExpr, sourceOrder: variableDict.Count); variableDict.Add(variable, v); } } } } finally { if (scope != null) scopeTracking.RemoveAt(scopeTracking.Count - 1); } } internal static bool VariableNeedsDeclaration(VariableKind kind) { switch (kind) { case VariableKind.PinnedRegionLocal: case VariableKind.Parameter: case VariableKind.ExceptionLocal: case VariableKind.ExceptionStackSlot: case VariableKind.UsingLocal: case VariableKind.ForeachLocal: return false; default: return true; } } /// /// Finds an insertion point in a common parent instruction. /// InsertionPoint FindCommonParent(InsertionPoint oldPoint, InsertionPoint newPoint) { // First ensure we're looking at nodes on the same level: oldPoint = oldPoint.UpTo(newPoint.level); newPoint = newPoint.UpTo(oldPoint.level); Debug.Assert(newPoint.level == oldPoint.level); // Then go up the tree until both points share the same parent: while (oldPoint.nextNode.Parent != newPoint.nextNode.Parent) { oldPoint = oldPoint.Up(); newPoint = newPoint.Up(); } // return oldPoint as that one comes first in the source code return oldPoint; } #endregion /// /// Some variable declarations in C# are illegal (colliding), /// even though the variable live ranges are not overlapping. /// /// Multiple declarations in same block: /// /// int i = 1; use(1); /// int i = 2; use(2); /// /// /// "Hiding" declaration in nested block: /// /// int i = 1; use(1); /// if (...) { /// int i = 2; use(2); /// } /// /// /// Nested blocks are illegal even if the parent block /// declares the variable later: /// /// if (...) { /// int i = 1; use(i); /// } /// int i = 2; use(i); /// /// /// ResolveCollisions() detects all these cases, and combines the variable declarations /// to a single declaration that is usable for the combined scopes. /// void ResolveCollisions() { var multiDict = new MultiDictionary(); foreach (var v in variableDict.Values) { // We can only insert variable declarations in blocks, but FindInsertionPoints() didn't // guarantee that it finds only blocks. // Fix that up now. while (!(v.InsertionPoint.nextNode.Parent is BlockStatement)) { if (v.InsertionPoint.nextNode.Parent is ForStatement f && v.InsertionPoint.nextNode == f.Initializers.FirstOrDefault() && IsMatchingAssignment(v, out _)) { // Special case: the initializer of a ForStatement can also declare a variable (with scope local to the for loop). break; } v.InsertionPoint = v.InsertionPoint.Up(); } // Note: 'out var', pattern matching etc. is not considered a valid insertion point here, because the scope of the // resulting variable is not restricted to the parent node of the insertion point, but extends to the whole BlockStatement. // We moved up the insertion point to the whole BlockStatement so that we can resolve collisions, // later we might decide to declare the variable more locally (as 'out var') instead if still possible. // Go through all potentially colliding variables: foreach (var prev in multiDict[v.Name]) { if (prev.RemovedDueToCollision) continue; // Go up until both nodes are on the same level: InsertionPoint point1 = prev.InsertionPoint.UpTo(v.InsertionPoint.level); InsertionPoint point2 = v.InsertionPoint.UpTo(prev.InsertionPoint.level); Debug.Assert(point1.level == point2.level); if (point1.nextNode.Parent == point2.nextNode.Parent) { // We found a collision! v.InvolvedInCollision = true; prev.ReplacementDueToCollision = v; // Continue checking other entries in multiDict against the new position of `v`. if (prev.SourceOrder < v.SourceOrder) { // Switch v's insertion point to prev's insertion point: v.InsertionPoint = point1; // Since prev was first, it has the correct SourceOrder/FirstUse values // for the new combined variable: v.SourceOrder = prev.SourceOrder; v.FirstUse = prev.FirstUse; } else { // v is first in source order, so it keeps its old insertion point // (and other properties), except that the insertion point is // moved up to prev's level. v.InsertionPoint = point2; } v.DefaultInitialization |= prev.DefaultInitialization; // I think we don't need to re-check the dict entries that we already checked earlier, // because the new v.InsertionPoint only collides with another point x if either // the old v.InsertionPoint or the old prev.InsertionPoint already collided with x. } } multiDict.Add(v.Name, v); } } private void InsertDeconstructionVariableDeclarations() { var usedVariables = new HashSet(); foreach (var g in variableDict.Values.GroupBy(v => v.InsertionPoint.nextNode)) { if (!(g.Key is ExpressionStatement { Expression: AssignmentExpression { Left: TupleExpression left, Operator: AssignmentOperatorType.Assign } assignment })) continue; usedVariables.Clear(); var deconstruct = assignment.Annotation(); if (deconstruct == null || deconstruct.Init.Count > 0 || deconstruct.Conversions.Instructions.Count > 0) continue; if (!deconstruct.Assignments.Instructions.All(IsDeclarableVariable)) continue; var designation = StatementBuilder.TranslateDeconstructionDesignation(deconstruct, isForeach: false); left.ReplaceWith(new DeclarationExpression { Type = new SimpleType("var"), Designation = designation }); foreach (var v in usedVariables) { variableDict[v].DeclaredInDeconstruction = true; } bool IsDeclarableVariable(ILInstruction inst) { if (!inst.MatchStLoc(out var v, out var value)) return false; if (!g.Any(vd => vd.ILVariable == v && !vd.RemovedDueToCollision)) return false; if (!usedVariables.Add(v)) return false; var expectedType = ((LdLoc)value).Variable.Type; if (!v.Type.Equals(expectedType)) return false; if (!(v.Kind == VariableKind.StackSlot || v.Kind == VariableKind.Local)) return false; return true; } } } bool IsMatchingAssignment(VariableToDeclare v, out AssignmentExpression assignment) { assignment = v.InsertionPoint.nextNode as AssignmentExpression; if (assignment == null) { assignment = (v.InsertionPoint.nextNode as ExpressionStatement)?.Expression as AssignmentExpression; if (assignment == null) return false; } return assignment.Operator == AssignmentOperatorType.Assign && assignment.Left is IdentifierExpression identExpr && identExpr.Identifier == v.Name && identExpr.TypeArguments.Count == 0; } bool CombineDeclarationAndInitializer(VariableToDeclare v, TransformContext context) { if (v.Type.IsByRefLike) return true; // by-ref-like variables always must be initialized at their declaration. if (v.InsertionPoint.nextNode.Role == ForStatement.InitializerRole) return true; // for-statement initializers always should combine declaration and initialization. return !context.Settings.SeparateLocalVariableDeclarations; } void InsertVariableDeclarations(TransformContext context) { var replacements = new List<(AstNode, AstNode)>(); foreach (var (ilVariable, v) in variableDict) { if (v.RemovedDueToCollision || v.DeclaredInDeconstruction) continue; if (CombineDeclarationAndInitializer(v, context) && IsMatchingAssignment(v, out AssignmentExpression assignment)) { // 'int v; v = expr;' can be combined to 'int v = expr;' AstType type; if (context.Settings.AnonymousTypes && v.Type.ContainsAnonymousType()) { type = new SimpleType("var"); } else { type = context.TypeSystemAstBuilder.ConvertType(v.Type); } if (v.ILVariable.IsRefReadOnly && type is ComposedType composedType && composedType.HasRefSpecifier) { composedType.HasReadOnlySpecifier = true; } if (v.ILVariable.Kind == VariableKind.PinnedLocal) { type.InsertChildAfter(null, new Comment("pinned", CommentType.MultiLine), Roles.Comment); } var vds = new VariableDeclarationStatement(type, v.Name, assignment.Right.Detach()); var init = vds.Variables.Single(); init.AddAnnotation(assignment.Left.GetResolveResult()); foreach (object annotation in assignment.Left.Annotations.Concat(assignment.Annotations)) { if (!(annotation is ResolveResult)) { init.AddAnnotation(annotation); } } replacements.Add((v.InsertionPoint.nextNode, vds)); } else if (CanBeDeclaredAsOutVariable(v, out var dirExpr)) { // 'T v; SomeCall(out v);' can be combined to 'SomeCall(out T v);' AstType type; if (context.Settings.AnonymousTypes && v.Type.ContainsAnonymousType()) { type = new SimpleType("var"); } else if (dirExpr.Annotation() != null) { type = new SimpleType("var"); } else { type = context.TypeSystemAstBuilder.ConvertType(v.Type); } string name; // Variable is not used and discards are allowed, we can simplify this to 'out T _'. // TODO: if no variable named _ is declared and var is used instead of T, use out _. // Note: ExpressionBuilder.HidesVariableWithName produces inaccurate results, because it // does not take lambdas and local functions into account, that are defined in the same // scope as v. if (context.Settings.Discards && v.ILVariable.LoadCount == 0 && v.ILVariable.StoreCount == 0 && v.ILVariable.AddressCount == 1) { name = "_"; } else { name = v.Name; } var ovd = new OutVarDeclarationExpression(type, name); ovd.Variable.AddAnnotation(new ILVariableResolveResult(ilVariable)); ovd.CopyAnnotationsFrom(dirExpr); replacements.Add((dirExpr, ovd)); } else { // Insert a separate declaration statement. Expression initializer = null; AstType type = context.TypeSystemAstBuilder.ConvertType(v.Type); if (v.DefaultInitialization) { initializer = new DefaultValueExpression(type.Clone()); } var vds = new VariableDeclarationStatement(type, v.Name, initializer); vds.Variables.Single().AddAnnotation(new ILVariableResolveResult(ilVariable)); Debug.Assert(v.InsertionPoint.nextNode.Role == BlockStatement.StatementRole); v.InsertionPoint.nextNode.Parent.InsertChildBefore( v.InsertionPoint.nextNode, vds, BlockStatement.StatementRole); } } // perform replacements at end, so that we don't replace a node while it is still referenced by a VariableToDeclare foreach (var (oldNode, newNode) in replacements) { oldNode.ReplaceWith(newNode); } } private bool CanBeDeclaredAsOutVariable(VariableToDeclare v, out DirectionExpression dirExpr) { dirExpr = v.FirstUse.Parent as DirectionExpression; if (dirExpr == null || dirExpr.FieldDirection != FieldDirection.Out) return false; if (!context.Settings.OutVariables) return false; if (v.DefaultInitialization) return false; for (AstNode node = v.FirstUse; node != null; node = node.Parent) { if (node.Role == Roles.EmbeddedStatement) { return false; } switch (node) { case IfElseStatement _: // variable declared in if condition appears in parent scope case ExpressionStatement _: return node == v.InsertionPoint.nextNode; case Statement _: return false; // other statements (e.g. while) don't allow variables to be promoted to parent scope } } return false; } /// /// Update ILVariableResolveResult annotations of all ILVariables that have been replaced by ResolveCollisions. /// void UpdateAnnotations(AstNode rootNode) { foreach (var node in rootNode.Descendants) { ILVariable ilVar; switch (node) { case IdentifierExpression id: ilVar = id.GetILVariable(); break; case VariableInitializer vi: ilVar = vi.GetILVariable(); break; default: continue; } if (ilVar == null || !VariableNeedsDeclaration(ilVar.Kind)) continue; var v = variableDict[ilVar]; if (!v.RemovedDueToCollision) continue; while (v.RemovedDueToCollision) { v = v.ReplacementDueToCollision; } node.RemoveAnnotations(); node.AddAnnotation(new ILVariableResolveResult(v.ILVariable, v.Type)); } } } }