Browse Source

[async] Decompile await operator.

pull/847/head
Daniel Grunwald 8 years ago
parent
commit
7d6122cfaf
  1. 4
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  2. 8
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  3. 1
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  4. 218
      ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs
  5. 6
      ICSharpCode.Decompiler/IL/Instructions.cs
  6. 3
      ICSharpCode.Decompiler/IL/Instructions.tt
  7. 15
      ICSharpCode.Decompiler/IL/Instructions/Await.cs
  8. 1
      ICSharpCode.Decompiler/IL/Instructions/ILVariableCollection.cs
  9. 32
      ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
  10. 3
      ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
  11. 4
      ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs

4
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -188,8 +188,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -188,8 +188,8 @@ namespace ICSharpCode.Decompiler.CSharp
return true;
if (settings.YieldReturn && YieldReturnDecompiler.IsCompilerGeneratorEnumerator(type))
return true;
// if (settings.AsyncAwait && AsyncDecompiler.IsCompilerGeneratedStateMachine(type))
// return true;
if (settings.AsyncAwait && AsyncAwaitDecompiler.IsCompilerGeneratedStateMachine(type))
return true;
} else if (type.IsCompilerGenerated()) {
// if (type.Name.StartsWith("<PrivateImplementationDetails>", StringComparison.Ordinal))
// return true;

8
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -1730,6 +1730,14 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1730,6 +1730,14 @@ namespace ICSharpCode.Decompiler.CSharp
.WithRR(new ByReferenceResolveResult(value.ResolveResult, false));
}
protected internal override TranslatedExpression VisitAwait(Await inst, TranslationContext context)
{
var value = Translate(inst.Value);
return new UnaryOperatorExpression(UnaryOperatorType.Await, value.Expression)
.WithILInstruction(inst)
.WithRR(new ResolveResult(inst?.GetResultMethod.ReturnType ?? SpecialType.UnknownType));
}
protected internal override TranslatedExpression VisitInvalidBranch(InvalidBranch inst, TranslationContext context)
{
string message = "Error";

1
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

@ -261,6 +261,7 @@ @@ -261,6 +261,7 @@
<Compile Include="IL\ControlFlow\ControlFlowGraph.cs" />
<Compile Include="IL\ControlFlow\StateRangeAnalysis.cs" />
<Compile Include="IL\ControlFlow\SymbolicExecution.cs" />
<Compile Include="IL\Instructions\Await.cs" />
<Compile Include="IL\Instructions\ILVariableCollection.cs" />
<Compile Include="IL\Instructions\NullCoalescingInstruction.cs" />
<Compile Include="IL\Patterns\AnyNode.cs" />

218
ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Util;
using System;
@ -15,18 +16,17 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -15,18 +16,17 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
/// </summary>
class AsyncAwaitDecompiler : IILTransform
{
/*
public static bool IsCompilerGeneratedStateMachine(TypeDefinition type)
public static bool IsCompilerGeneratedStateMachine(Mono.Cecil.TypeDefinition type)
{
if (!(type.DeclaringType != null && type.IsCompilerGenerated()))
return false;
foreach (TypeReference i in type.Interfaces) {
if (i.Namespace == "System.Runtime.CompilerServices" && i.Name == "IAsyncStateMachine")
foreach (var i in type.Interfaces) {
var iface = i.InterfaceType;
if (iface.Namespace == "System.Runtime.CompilerServices" && iface.Name == "IAsyncStateMachine")
return true;
}
return false;
}
*/
enum AsyncMethodType
{
@ -55,26 +55,42 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -55,26 +55,42 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
Block setResultAndExitBlock; // block that is jumped to for return statements
int finalState; // final state after the setResultAndExitBlock
ILVariable resultVar; // the variable that gets returned by the setResultAndExitBlock
ILVariable doFinallyBodies;
// These fields are set by AnalyzeStateMachine():
// For each block containing an 'await', stores the awaiter variable, and the field storing the awaiter
// across the yield point.
Dictionary<Block, (ILVariable awaiterVar, IField awaiterField)> awaitBlocks = new Dictionary<Block, (ILVariable awaiterVar, IField awaiterField)>();
public void Run(ILFunction function, ILTransformContext context)
{
if (!context.Settings.AsyncAwait)
return; // abort if async/await decompilation is disabled
this.context = context;
fieldToParameterMap.Clear();
awaitBlocks.Clear();
if (!MatchTaskCreationPattern(function))
return;
try {
AnalyzeMoveNext();
ValidateCatchBlock();
InlineBodyOfMoveNext(function);
AnalyzeStateMachine(function);
FinalizeInlineMoveNext(function);
} catch (SymbolicAnalysisFailedException) {
return;
}
InlineBodyOfMoveNext(function);
AnalyzeStateMachine(function);
DetectAwaitPattern(function);
context.Step("Translate fields to local accesses", function);
YieldReturnDecompiler.TranslateFieldsToLocalAccess(function, function, fieldToParameterMap);
FinalizeInlineMoveNext(function);
// Re-run control flow simplification over the newly constructed set of gotos,
// and inlining because TranslateFieldsToLocalAccess() might have opened up new inlining opportunities.
function.RunTransforms(CSharpDecompiler.EarlyILTransforms(), context);
}
#region MatchTaskCreationPattern
@ -396,6 +412,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -396,6 +412,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
}
function.Variables.AddRange(function.Descendants.OfType<IInstructionWithVariableOperand>().Select(inst => inst.Variable).Distinct());
function.Variables.RemoveDead();
function.Variables.AddRange(fieldToParameterMap.Values);
}
void FinalizeInlineMoveNext(ILFunction function)
@ -412,6 +429,8 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -412,6 +429,8 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
}
#endregion
#region AnalyzeStateMachine
/// <summary>
/// Analyze the the state machine; and replace 'leave IL_0000' with await+jump to block that gets
/// entered on the next MoveNext() call.
@ -427,16 +446,18 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -427,16 +446,18 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
sra.AssignStateRanges(container, LongSet.Universe);
foreach (var block in container.Blocks) {
context.CancellationToken.ThrowIfCancellationRequested();
if (block.Instructions.Last().MatchLeave((BlockContainer)moveNextFunction.Body)) {
// This is likely an 'await' block
if (AnalyzeAwaitBlock(block, out var awaiter, out var awaiterField, out var state)) {
block.Instructions.Add(new Await(new LdLoca(awaiter)));
if (AnalyzeAwaitBlock(block, out var awaiterVar, out var awaiterField, out var state)) {
block.Instructions.Add(new Await(new LdLoca(awaiterVar)));
Block targetBlock = sra.FindBlock(container, state);
if (targetBlock != null) {
block.Instructions.Add(new Branch(targetBlock));
} else {
block.Instructions.Add(new InvalidBranch("Could not find block for state " + state));
}
awaitBlocks.Add(block, (awaiterVar, awaiterField));
}
}
}
@ -458,7 +479,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -458,7 +479,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
awaiter = null;
awaiterField = null;
state = 0;
context.CancellationToken.ThrowIfCancellationRequested();
int pos = block.Instructions.Count - 2;
if (doFinallyBodies != null && block.Instructions[pos] is StLoc storeDoFinallyBodies) {
if (!(storeDoFinallyBodies.Variable.Kind == VariableKind.Local
@ -525,7 +545,9 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -525,7 +545,9 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
// delete preceding dead stores:
while (pos > 0 && block.Instructions[pos - 1] is StLoc stloc2
&& stloc2.Variable.IsSingleDefinition && stloc2.Variable.LoadCount == 0
&& stloc2.Variable.Kind == VariableKind.StackSlot) {
&& stloc2.Variable.Kind == VariableKind.StackSlot
&& SemanticHelper.IsPure(stloc2.Value.Flags))
{
pos--;
}
block.Instructions.RemoveRange(pos, block.Instructions.Count - pos);
@ -541,5 +563,173 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -541,5 +563,173 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
}
return inst;
}
#endregion
#region DetectAwaitPattern
void DetectAwaitPattern(ILFunction function)
{
context.StepStartGroup("DetectAwaitPattern", function);
foreach (var container in function.Descendants.OfType<BlockContainer>()) {
foreach (var block in container.Blocks) {
context.CancellationToken.ThrowIfCancellationRequested();
DetectAwaitPattern(block);
}
container.SortBlocks(deleteUnreachableBlocks: true);
}
context.StepEndGroup(keepIfEmpty: true);
}
void DetectAwaitPattern(Block block)
{
// block:
// stloc awaiterVar(callvirt GetAwaiter(...))
// if (call get_IsCompleted(ldloca awaiterVar)) br completedBlock
// br awaitBlock
// awaitBlock:
// ..
// br resumeBlock
// resumeBlock:
// ..
// br completedBlock
if (block.Instructions.Count < 3)
return;
// stloc awaiterVar(callvirt GetAwaiter(...))
if (!(block.Instructions[block.Instructions.Count - 3] is StLoc stLocAwaiter))
return;
ILVariable awaiterVar = stLocAwaiter.Variable;
if (!(stLocAwaiter.Value is CallInstruction getAwaiterCall))
return;
if (!(getAwaiterCall.Method.Name == "GetAwaiter" && (!getAwaiterCall.Method.IsStatic || getAwaiterCall.Method.IsExtensionMethod)))
return;
if (getAwaiterCall.Arguments.Count != 1)
return;
// if (call get_IsCompleted(ldloca awaiterVar)) br completedBlock
if (!block.Instructions[block.Instructions.Count - 2].MatchIfInstruction(out var condition, out var trueInst))
return;
if (!MatchCall(condition, "get_IsCompleted", out var isCompletedArgs) || isCompletedArgs.Count != 1)
return;
if (!isCompletedArgs[0].MatchLdLocRef(awaiterVar))
return;
if (!trueInst.MatchBranch(out var completedBlock))
return;
// br awaitBlock
if (!block.Instructions.Last().MatchBranch(out var awaitBlock))
return;
// Check awaitBlock and resumeBlock:
if (!awaitBlocks.TryGetValue(awaitBlock, out var awaitBlockData))
return;
if (awaitBlockData.awaiterVar != awaiterVar)
return;
if (!CheckAwaitBlock(awaitBlock, out var resumeBlock))
return;
if (!CheckResumeBlock(resumeBlock, awaiterVar, awaitBlockData.awaiterField, completedBlock))
return;
// Check completedBlock:
ILInstruction getResultCall;
if (completedBlock.Instructions[0] is StLoc resultStore) {
getResultCall = resultStore.Value;
} else {
getResultCall = completedBlock.Instructions[0];
resultStore = null;
}
if (!MatchCall(getResultCall, "GetResult", out var getResultArgs) || getResultArgs.Count != 1)
return;
if (!getResultArgs[0].MatchLdLocRef(awaiterVar))
return;
// All checks successful, let's transform.
context.Step("Transform await pattern", block);
block.Instructions.RemoveAt(block.Instructions.Count - 3); // remove getAwaiter call
block.Instructions.RemoveAt(block.Instructions.Count - 2); // remove if (isCompleted)
((Branch)block.Instructions.Last()).TargetBlock = completedBlock; // instead, directly jump to completed block
Await awaitInst = new Await(getAwaiterCall.Arguments.Single());
awaitInst.GetResultMethod = ((CallInstruction)getResultCall).Method;
awaitInst.GetAwaiterMethod = getAwaiterCall.Method;
if (resultStore != null) {
resultStore.Value = awaitInst;
} else {
completedBlock.Instructions[0] = awaitInst;
}
// Remove useless reset of awaiterVar.
if (completedBlock.Instructions[1] is StObj stobj) {
if (stobj.Target.MatchLdLoca(awaiterVar) && stobj.Value.OpCode == OpCode.DefaultValue) {
completedBlock.Instructions.RemoveAt(1);
}
}
}
bool CheckAwaitBlock(Block block, out Block resumeBlock)
{
// awaitBlock:
// await(ldloca V_2)
// br resumeBlock
resumeBlock = null;
if (block.Instructions.Count != 2)
return false;
if (block.Instructions[0].OpCode != OpCode.Await)
return false;
if (!block.Instructions[1].MatchBranch(out resumeBlock))
return false;
return true;
}
bool CheckResumeBlock(Block block, ILVariable awaiterVar, IField awaiterField, Block completedBlock)
{
int pos = 0;
// stloc awaiterVar(ldfld awaiterField(ldloc this))
if (!block.Instructions[pos].MatchStLoc(awaiterVar, out var value))
return false;
if (!value.MatchLdFld(out var target, out var field))
return false;
if (!target.MatchLdThis())
return false;
if (field.MemberDefinition != awaiterField)
return false;
pos++;
// stfld awaiterField(ldloc this, default.value)
if (block.Instructions[pos].MatchStFld(out target, out field, out value)
&& target.MatchLdThis()
&& field.MemberDefinition == awaiterField
&& value.OpCode == OpCode.DefaultValue)
{
pos++;
}
// stloc S_27(ldloc this)
// stloc S_28(ldc.i4 -1)
// stloc cachedStateVar(ldloc S_28)
// stfld <>1__state(ldloc S_27, ldloc S_28)
ILVariable thisVar = null;
if (block.Instructions[pos] is StLoc stlocThis && stlocThis.Value.MatchLdThis() && stlocThis.Variable.Kind == VariableKind.StackSlot) {
thisVar = stlocThis.Variable;
pos++;
}
ILVariable m1Var = null;
if (block.Instructions[pos] is StLoc stlocM1 && stlocM1.Value.MatchLdcI4(initialState) && stlocM1.Variable.Kind == VariableKind.StackSlot) {
m1Var = stlocM1.Variable;
pos++;
}
if (block.Instructions[pos] is StLoc stlocCachedState) {
if (stlocCachedState.Variable.Kind == VariableKind.Local && stlocCachedState.Variable.Index == cachedStateVar.Index) {
if (stlocCachedState.Value.MatchLdLoc(m1Var) || stlocCachedState.Value.MatchLdcI4(initialState))
pos++;
}
}
if (block.Instructions[pos].MatchStFld(out target, out field, out value)) {
if (!(target.MatchLdThis() || target.MatchLdLoc(thisVar)))
return false;
if (field.MemberDefinition != stateField)
return false;
if (!(value.MatchLdcI4(initialState) || value.MatchLdLoc(m1Var)))
return false;
pos++;
} else {
return false;
}
return block.Instructions[pos].MatchBranch(completedBlock);
}
#endregion
}
}

6
ICSharpCode.Decompiler/IL/Instructions.cs

@ -727,6 +727,7 @@ namespace ICSharpCode.Decompiler.IL @@ -727,6 +727,7 @@ namespace ICSharpCode.Decompiler.IL
{
base.CheckInvariant(phase);
Debug.Assert(phase <= ILPhase.InILReader || this.IsDescendantOf(variable.Function));
Debug.Assert(phase <= ILPhase.InILReader || variable.Function.Variables[variable.IndexInFunction] == variable);
}
public static readonly SlotInfo InitSlot = new SlotInfo("Init", canInlineInto: true);
ILInstruction init;
@ -1790,6 +1791,7 @@ namespace ICSharpCode.Decompiler.IL @@ -1790,6 +1791,7 @@ namespace ICSharpCode.Decompiler.IL
{
base.CheckInvariant(phase);
Debug.Assert(phase <= ILPhase.InILReader || this.IsDescendantOf(variable.Function));
Debug.Assert(phase <= ILPhase.InILReader || variable.Function.Variables[variable.IndexInFunction] == variable);
}
public override StackType ResultType { get { return variable.StackType; } }
protected override InstructionFlags ComputeFlags()
@ -1873,6 +1875,7 @@ namespace ICSharpCode.Decompiler.IL @@ -1873,6 +1875,7 @@ namespace ICSharpCode.Decompiler.IL
{
base.CheckInvariant(phase);
Debug.Assert(phase <= ILPhase.InILReader || this.IsDescendantOf(variable.Function));
Debug.Assert(phase <= ILPhase.InILReader || variable.Function.Variables[variable.IndexInFunction] == variable);
}
public override void WriteTo(ITextOutput output)
{
@ -1946,6 +1949,7 @@ namespace ICSharpCode.Decompiler.IL @@ -1946,6 +1949,7 @@ namespace ICSharpCode.Decompiler.IL
{
base.CheckInvariant(phase);
Debug.Assert(phase <= ILPhase.InILReader || this.IsDescendantOf(variable.Function));
Debug.Assert(phase <= ILPhase.InILReader || variable.Function.Variables[variable.IndexInFunction] == variable);
}
public static readonly SlotInfo ValueSlot = new SlotInfo("Value", canInlineInto: true);
ILInstruction value;
@ -4053,7 +4057,7 @@ namespace ICSharpCode.Decompiler.IL @@ -4053,7 +4057,7 @@ namespace ICSharpCode.Decompiler.IL
clone.Value = this.value.Clone();
return clone;
}
public override StackType ResultType { get { return StackType.Void; } }
public override StackType ResultType { get { return GetResultMethod?.ReturnType.GetStackType() ?? StackType.Unknown; } }
protected override InstructionFlags ComputeFlags()
{
return InstructionFlags.SideEffect | value.Flags;

3
ICSharpCode.Decompiler/IL/Instructions.tt

@ -225,7 +225,7 @@ @@ -225,7 +225,7 @@
// note: "yield break" is always represented using a "leave" instruction
new OpCode("await", "C# await operator.",
SideEffect, // other code can run with arbitrary side effects while we're waiting
CustomArguments("value"), ResultType("Void")),
CustomArguments("value"), ResultType("GetResultMethod?.ReturnType.GetStackType() ?? StackType.Unknown")),
// patterns
new OpCode("AnyNode", "Matches any node", Pattern, CustomArguments(), CustomConstructor),
@ -869,6 +869,7 @@ protected override void Disconnected() @@ -869,6 +869,7 @@ protected override void Disconnected()
{
base.CheckInvariant(phase);
Debug.Assert(phase <= ILPhase.InILReader || this.IsDescendantOf(variable.Function));
Debug.Assert(phase <= ILPhase.InILReader || variable.Function.Variables[variable.IndexInFunction] == variable);
}");
}
};

15
ICSharpCode.Decompiler/IL/Instructions/Await.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using ICSharpCode.Decompiler.TypeSystem;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ICSharpCode.Decompiler.IL
{
partial class Await
{
public IMethod GetAwaiterMethod;
public IMethod GetResultMethod;
}
}

1
ICSharpCode.Decompiler/IL/Instructions/ILVariableCollection.cs

@ -88,6 +88,7 @@ namespace ICSharpCode.Decompiler.IL @@ -88,6 +88,7 @@ namespace ICSharpCode.Decompiler.IL
void RemoveAt(int index)
{
list[index].Function = null;
// swap-remove index
list[index] = list[list.Count - 1];
list[index].IndexInFunction = index;

32
ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs

@ -216,27 +216,33 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -216,27 +216,33 @@ namespace ICSharpCode.Decompiler.IL.Transforms
protected internal override void VisitStObj(StObj inst)
{
base.VisitStObj(inst);
ILVariable v;
if (inst.Target.MatchLdLoca(out v)
&& TypeUtils.IsCompatibleTypeForMemoryAccess(new ByReferenceType(v.Type), inst.Type)
&& inst.UnalignedPrefix == 0
&& !inst.IsVolatile)
{
context.Step("stobj(ldloca(v), ...) => stloc(v, ...)", inst);
inst.ReplaceWith(new StLoc(v, inst.Value));
if (StObjToStLoc(inst, context)) {
return;
}
ILInstruction target;
IType t;
BinaryNumericInstruction binary = inst.Value as BinaryNumericInstruction;
if (binary != null && binary.Left.MatchLdObj(out target, out t) && inst.Target.Match(target).Success) {
if (inst.Value is BinaryNumericInstruction binary
&& binary.Left.MatchLdObj(out ILInstruction target, out IType t)
&& inst.Target.Match(target).Success)
{
context.Step("compound assignment", inst);
// stobj(target, binary.op(ldobj(target), ...))
// => compound.op(target, ...)
inst.ReplaceWith(new CompoundAssignmentInstruction(binary.Operator, binary.Left, binary.Right, t, binary.CheckForOverflow, binary.Sign, CompoundAssignmentType.EvaluatesToNewValue));
}
}
internal static bool StObjToStLoc(StObj inst, ILTransformContext context)
{
if (inst.Target.MatchLdLoca(out ILVariable v)
&& TypeUtils.IsCompatibleTypeForMemoryAccess(new ByReferenceType(v.Type), inst.Type)
&& inst.UnalignedPrefix == 0
&& !inst.IsVolatile) {
context.Step("stobj(ldloca(v), ...) => stloc(v, ...)", inst);
inst.ReplaceWith(new StLoc(v, inst.Value));
return true;
}
return false;
}
protected internal override void VisitIfInstruction(IfInstruction inst)
{

3
ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs

@ -261,10 +261,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -261,10 +261,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms
// decide based on the source expression being inlined
switch (inlinedExpression.OpCode) {
case OpCode.DefaultValue:
return true;
case OpCode.StObj:
return true;
case OpCode.CompoundAssignmentInstruction:
case OpCode.Await:
return true;
case OpCode.LdLoc:
if (v.StateMachineField == null && ((LdLoc)inlinedExpression).Variable.StateMachineField != null) {

4
ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs

@ -37,8 +37,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -37,8 +37,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
v.HasInitialValue = false;
}
}
if (function.IsIterator) {
// In yield return, the C# compiler tends to store null/default(T) to variables
if (function.IsIterator || function.IsAsync) {
// In yield return + async, the C# compiler tends to store null/default(T) to variables
// when the variable goes out of scope. Remove such useless stores.
foreach (var v in function.Variables) {
if (v.Kind == VariableKind.Local && v.StoreCount == 1 && v.LoadCount == 0 && v.AddressCount == 0) {

Loading…
Cancel
Save