Browse Source

#1050: Add support for ?. operator applied to ref-parameters, and other cases where the compiler uses a generated ref local for the ?. operator.

Still not supported: ?. operator applied to a ref to unconstrained generic type.
pull/1072/merge
Daniel Grunwald 8 years ago
parent
commit
4177e182fe
  1. 17
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs
  2. 54
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.opt.roslyn.il
  3. 66
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.roslyn.il
  4. 3
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  5. 52
      ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs
  6. 118
      ICSharpCode.Decompiler/IL/Transforms/NullPropagationTransform.cs

17
ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs

@ -230,5 +230,22 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -230,5 +230,22 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
return t?.Int();
}
// See also: https://github.com/icsharpcode/ILSpy/issues/1050
// The C# compiler generates pretty weird code in this case.
//private static int? GenericRefUnconstrainedInt<T>(ref T t) where T : ITest
//{
// return t?.Int();
//}
private static int? GenericRefClassConstraintInt<T>(ref T t) where T : class, ITest
{
return t?.Int();
}
private static int? GenericRefStructConstraintInt<T>(ref T? t) where T : struct, ITest
{
return t?.Int();
}
}
}

54
ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.opt.roslyn.il

@ -25,14 +25,14 @@ @@ -25,14 +25,14 @@
.ver 0:0:0:0
}
.module NullPropagation.dll
// MVID: {B228D113-C048-4D76-A7D1-78312CF90769}
// MVID: {DCAB73BC-6F5B-48B0-B0CB-6AD8459C2BC9}
.custom instance void [mscorlib]System.Security.UnverifiableCodeAttribute::.ctor() = ( 01 00 00 00 )
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WINDOWS_CUI
.corflags 0x00000001 // ILONLY
// Image base: 0x03040000
// Image base: 0x050B0000
// =============== CLASS MEMBERS DECLARATION ===================
@ -930,6 +930,56 @@ @@ -930,6 +930,56 @@
IL_002d: ret
} // end of method NullPropagation::GenericStructConstraintInt
.method private hidebysig static valuetype [mscorlib]System.Nullable`1<int32>
GenericRefClassConstraintInt<class (ICSharpCode.Decompiler.Tests.TestCases.Pretty.NullPropagation/ITest) T>(!!T& t) cil managed
{
// Code size 36 (0x24)
.maxstack 2
.locals init (valuetype [mscorlib]System.Nullable`1<int32> V_0)
IL_0000: ldarg.0
IL_0001: ldobj !!T
IL_0006: box !!T
IL_000b: dup
IL_000c: brtrue.s IL_0019
IL_000e: pop
IL_000f: ldloca.s V_0
IL_0011: initobj valuetype [mscorlib]System.Nullable`1<int32>
IL_0017: ldloc.0
IL_0018: ret
IL_0019: callvirt instance int32 ICSharpCode.Decompiler.Tests.TestCases.Pretty.NullPropagation/ITest::Int()
IL_001e: newobj instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_0023: ret
} // end of method NullPropagation::GenericRefClassConstraintInt
.method private hidebysig static valuetype [mscorlib]System.Nullable`1<int32>
GenericRefStructConstraintInt<valuetype .ctor (ICSharpCode.Decompiler.Tests.TestCases.Pretty.NullPropagation/ITest, [mscorlib]System.ValueType) T>(valuetype [mscorlib]System.Nullable`1<!!T>& t) cil managed
{
// Code size 45 (0x2d)
.maxstack 2
.locals init (valuetype [mscorlib]System.Nullable`1<int32> V_0,
!!T V_1)
IL_0000: ldarg.0
IL_0001: dup
IL_0002: call instance bool valuetype [mscorlib]System.Nullable`1<!!T>::get_HasValue()
IL_0007: brtrue.s IL_0014
IL_0009: pop
IL_000a: ldloca.s V_0
IL_000c: initobj valuetype [mscorlib]System.Nullable`1<int32>
IL_0012: ldloc.0
IL_0013: ret
IL_0014: call instance !0 valuetype [mscorlib]System.Nullable`1<!!T>::GetValueOrDefault()
IL_0019: stloc.1
IL_001a: ldloca.s V_1
IL_001c: constrained. !!T
IL_0022: callvirt instance int32 ICSharpCode.Decompiler.Tests.TestCases.Pretty.NullPropagation/ITest::Int()
IL_0027: newobj instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_002c: ret
} // end of method NullPropagation::GenericRefStructConstraintInt
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{

66
ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.roslyn.il

@ -25,14 +25,14 @@ @@ -25,14 +25,14 @@
.ver 0:0:0:0
}
.module NullPropagation.dll
// MVID: {AC5F707E-C73C-480F-9B50-4CCACD230B55}
// MVID: {0DD72CB6-BC75-43B0-A076-30539A83A77C}
.custom instance void [mscorlib]System.Security.UnverifiableCodeAttribute::.ctor() = ( 01 00 00 00 )
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WINDOWS_CUI
.corflags 0x00000001 // ILONLY
// Image base: 0x04950000
// Image base: 0x03540000
// =============== CLASS MEMBERS DECLARATION ===================
@ -1108,6 +1108,68 @@ @@ -1108,6 +1108,68 @@
IL_0033: ret
} // end of method NullPropagation::GenericStructConstraintInt
.method private hidebysig static valuetype [mscorlib]System.Nullable`1<int32>
GenericRefClassConstraintInt<class (ICSharpCode.Decompiler.Tests.TestCases.Pretty.NullPropagation/ITest) T>(!!T& t) cil managed
{
// Code size 42 (0x2a)
.maxstack 2
.locals init (valuetype [mscorlib]System.Nullable`1<int32> V_0,
valuetype [mscorlib]System.Nullable`1<int32> V_1)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldobj !!T
IL_0007: box !!T
IL_000c: dup
IL_000d: brtrue.s IL_001b
IL_000f: pop
IL_0010: ldloca.s V_0
IL_0012: initobj valuetype [mscorlib]System.Nullable`1<int32>
IL_0018: ldloc.0
IL_0019: br.s IL_0025
IL_001b: callvirt instance int32 ICSharpCode.Decompiler.Tests.TestCases.Pretty.NullPropagation/ITest::Int()
IL_0020: newobj instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_0025: stloc.1
IL_0026: br.s IL_0028
IL_0028: ldloc.1
IL_0029: ret
} // end of method NullPropagation::GenericRefClassConstraintInt
.method private hidebysig static valuetype [mscorlib]System.Nullable`1<int32>
GenericRefStructConstraintInt<valuetype .ctor (ICSharpCode.Decompiler.Tests.TestCases.Pretty.NullPropagation/ITest, [mscorlib]System.ValueType) T>(valuetype [mscorlib]System.Nullable`1<!!T>& t) cil managed
{
// Code size 51 (0x33)
.maxstack 2
.locals init (valuetype [mscorlib]System.Nullable`1<int32> V_0,
!!T V_1,
valuetype [mscorlib]System.Nullable`1<int32> V_2)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: dup
IL_0003: call instance bool valuetype [mscorlib]System.Nullable`1<!!T>::get_HasValue()
IL_0008: brtrue.s IL_0016
IL_000a: pop
IL_000b: ldloca.s V_0
IL_000d: initobj valuetype [mscorlib]System.Nullable`1<int32>
IL_0013: ldloc.0
IL_0014: br.s IL_002e
IL_0016: call instance !0 valuetype [mscorlib]System.Nullable`1<!!T>::GetValueOrDefault()
IL_001b: stloc.1
IL_001c: ldloca.s V_1
IL_001e: constrained. !!T
IL_0024: callvirt instance int32 ICSharpCode.Decompiler.Tests.TestCases.Pretty.NullPropagation/ITest::Int()
IL_0029: newobj instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_002e: stloc.2
IL_002f: br.s IL_0031
IL_0031: ldloc.2
IL_0032: ret
} // end of method NullPropagation::GenericRefStructConstraintInt
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{

3
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -2172,6 +2172,9 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2172,6 +2172,9 @@ namespace ICSharpCode.Decompiler.CSharp
protected internal override TranslatedExpression VisitNullableUnwrap(NullableUnwrap inst, TranslationContext context)
{
var arg = Translate(inst.Argument);
if (inst.RefInput && !inst.RefOutput && arg.Expression is DirectionExpression dir) {
arg = arg.UnwrapChild(dir.Expression);
}
return new UnaryOperatorExpression(UnaryOperatorType.NullConditional, arg)
.WithILInstruction(inst)
.WithRR(new ResolveResult(NullableType.GetUnderlyingType(arg.Type)));

52
ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs

@ -21,18 +21,61 @@ using System.Linq; @@ -21,18 +21,61 @@ using System.Linq;
namespace ICSharpCode.Decompiler.IL
{
/// <summary>
/// For a nullable input, gets the underlying value.
///
/// There are three possible input types:
/// * reference type: if input!=null, evaluates to the input
/// * nullable value type: if input.Has_Value, evaluates to input.GetValueOrDefault()
/// * generic type: behavior depends on the type at runtime.
/// If non-nullable value type, unconditionally evaluates to the input.
///
/// If the input is null, control-flow is tranferred to the nearest surrounding nullable.rewrap
/// instruction.
/// </summary>
partial class NullableUnwrap
{
public NullableUnwrap(StackType unwrappedType, ILInstruction argument)
/// <summary>
/// Whether the argument is dereferenced before checking for a null input.
/// If true, the argument must be a managed reference to a valid input type.
/// </summary>
/// <remarks>
/// This mode exists because the C# compiler sometimes avoids copying the whole Nullable{T} struct
/// before the null-check.
/// The underlying struct T is still copied by the GetValueOrDefault() call, but only in the non-null case.
/// </remarks>
public readonly bool RefInput;
/// <summary>
/// Consider the following code generated for <code>t?.Method()</code> on a generic t:
/// <code>if (comp(box ``0(ldloc t) != ldnull)) newobj Nullable..ctor(constrained[``0].callvirt Method(ldloca t)) else default.value Nullable</code>
/// Here, the method is called on the original reference, and any mutations performed by the method will be visible in the original variable.
///
/// To represent this, we use a nullable.unwrap with ResultType==Ref: instead of returning the input value,
/// the input reference is returned in the non-null case.
/// Note that in case the generic type ends up being <c>Nullable{T}</c>, this means methods will end up being called on
/// the nullable type, not on the underlying type. However, this ends up making no difference, because the only methods
/// that can be called that way are those on System.Object. All the virtual methods are overridden in <c>Nullable{T}</c>
/// and end up forwarding to <c>T</c>; and the non-virtual methods cause boxing which strips the <c>Nullable{T}</c> wrapper.
///
/// RefOutput can only be used if RefInput is also used.
/// </summary>
public bool RefOutput { get => ResultType == StackType.Ref; }
public NullableUnwrap(StackType unwrappedType, ILInstruction argument, bool refInput=false)
: base(OpCode.NullableUnwrap, argument)
{
this.ResultType = unwrappedType;
this.RefInput = refInput;
if (unwrappedType == StackType.Ref) {
Debug.Assert(refInput);
}
}
internal override void CheckInvariant(ILPhase phase)
{
base.CheckInvariant(phase);
if (this.ResultType == StackType.Ref) {
if (this.RefInput) {
Debug.Assert(Argument.ResultType == StackType.Ref, "nullable.unwrap expects reference to nullable type as input");
} else {
Debug.Assert(Argument.ResultType == StackType.O, "nullable.unwrap expects nullable type as input");
@ -42,7 +85,10 @@ namespace ICSharpCode.Decompiler.IL @@ -42,7 +85,10 @@ namespace ICSharpCode.Decompiler.IL
public override void WriteTo(ITextOutput output, ILAstWritingOptions options)
{
output.Write("nullable.unwrap ");
output.Write("nullable.unwrap.");
if (RefInput) {
output.Write("refinput.");
}
output.Write(ResultType);
output.Write('(');
Argument.WriteTo(output, options);

118
ICSharpCode.Decompiler/IL/Transforms/NullPropagationTransform.cs

@ -46,6 +46,22 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -46,6 +46,22 @@ namespace ICSharpCode.Decompiler.IL.Transforms
this.context = context;
}
enum Mode
{
/// <summary>
/// reference type or generic type (comparison is 'comp(ldloc(testedVar) == null)')
/// </summary>
ReferenceType,
/// <summary>
/// nullable type, used by value (comparison is 'call get_HasValue(ldloca(testedVar))')
/// </summary>
NullableByValue,
/// <summary>
/// nullable type, used by reference (comparison is 'call get_HasValue(ldloc(testedVar))')
/// </summary>
NullableByReference,
}
/// <summary>
/// Check if "condition ? trueInst : falseInst" can be simplified using the null-conditional operator.
/// Returns the replacement instruction, or null if no replacement is possible.
@ -59,13 +75,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -59,13 +75,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return null;
if (comp.Kind == ComparisonKind.Equality) {
// testedVar == null ? trueInst : falseInst
return TryNullPropagation(testedVar, falseInst, trueInst, true, ilRange);
return TryNullPropagation(testedVar, falseInst, trueInst, Mode.ReferenceType, ilRange);
} else if (comp.Kind == ComparisonKind.Inequality) {
return TryNullPropagation(testedVar, trueInst, falseInst, true, ilRange);
return TryNullPropagation(testedVar, trueInst, falseInst, Mode.ReferenceType, ilRange);
}
} else if (NullableLiftingTransform.MatchHasValueCall(condition, out ILInstruction loadInst)) {
// loadInst.HasValue ? trueInst : falseInst
if (loadInst.MatchLdLoca(out testedVar)) {
return TryNullPropagation(testedVar, trueInst, falseInst, Mode.NullableByValue, ilRange);
} else if (loadInst.MatchLdLoc(out testedVar)) {
return TryNullPropagation(testedVar, trueInst, falseInst, Mode.NullableByReference, ilRange);
}
} else if (NullableLiftingTransform.MatchHasValueCall(condition, out testedVar)) {
// testedVar.HasValue ? trueInst : falseInst
return TryNullPropagation(testedVar, trueInst, falseInst, false, ilRange);
}
return null;
}
@ -74,7 +94,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -74,7 +94,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// testedVar != null ? nonNullInst : nullInst
/// </summary>
ILInstruction TryNullPropagation(ILVariable testedVar, ILInstruction nonNullInst, ILInstruction nullInst,
bool testedVarHasReferenceType, Interval ilRange)
Mode mode, Interval ilRange)
{
bool removedRewrapOrNullableCtor = false;
if (NullableLiftingTransform.MatchNullableCtor(nonNullInst, out _, out var arg)) {
@ -84,28 +104,28 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -84,28 +104,28 @@ namespace ICSharpCode.Decompiler.IL.Transforms
nonNullInst = arg;
removedRewrapOrNullableCtor = true;
}
if (!IsValidAccessChain(testedVar, testedVarHasReferenceType, nonNullInst, out var varLoad))
if (!IsValidAccessChain(testedVar, mode, nonNullInst, out var varLoad))
return null;
// note: InferType will be accurate in this case because the access chain consists of calls and field accesses
IType returnType = nonNullInst.InferType();
if (nullInst.MatchLdNull()) {
context.Step("Null propagation (reference type)", nonNullInst);
context.Step($"Null propagation (mode={mode}, output=reference type)", nonNullInst);
// testedVar != null ? testedVar.AccessChain : null
// => testedVar?.AccessChain
IntroduceUnwrap(testedVar, varLoad);
IntroduceUnwrap(testedVar, varLoad, mode);
return new NullableRewrap(nonNullInst) { ILRange = ilRange };
} else if (nullInst.MatchDefaultValue(out var type) && type.IsKnownType(KnownTypeCode.NullableOfT)) {
context.Step("Null propagation (value type)", nonNullInst);
context.Step($"Null propagation (mode={mode}, output=value type)", nonNullInst);
// testedVar != null ? testedVar.AccessChain : default(T?)
// => testedVar?.AccessChain
IntroduceUnwrap(testedVar, varLoad);
IntroduceUnwrap(testedVar, varLoad, mode);
return new NullableRewrap(nonNullInst) { ILRange = ilRange };
} else if (!removedRewrapOrNullableCtor && NullableType.IsNonNullableValueType(returnType)) {
context.Step("Null propagation with null coalescing", nonNullInst);
context.Step($"Null propagation (mode={mode}, output=null coalescing)", nonNullInst);
// testedVar != null ? testedVar.AccessChain : nullInst
// => testedVar?.AccessChain ?? nullInst
// (only valid if AccessChain returns a non-nullable value)
IntroduceUnwrap(testedVar, varLoad);
IntroduceUnwrap(testedVar, varLoad, mode);
return new NullCoalescingInstruction(
NullCoalescingKind.NullableWithValueFallback,
new NullableRewrap(nonNullInst),
@ -129,13 +149,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -129,13 +149,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return;
if (ifInst.Condition is Comp comp && comp.Kind == ComparisonKind.Inequality
&& comp.Left.MatchLdLoc(out var testedVar) && comp.Right.MatchLdNull()) {
TryNullPropForVoidCall(testedVar, true, ifInst.TrueInst as Block, ifInst);
} else if (NullableLiftingTransform.MatchHasValueCall(ifInst.Condition, out testedVar)) {
TryNullPropForVoidCall(testedVar, false, ifInst.TrueInst as Block, ifInst);
TryNullPropForVoidCall(testedVar, Mode.ReferenceType, ifInst.TrueInst as Block, ifInst);
} else if (NullableLiftingTransform.MatchHasValueCall(ifInst.Condition, out ILInstruction arg)) {
if (arg.MatchLdLoca(out testedVar)) {
TryNullPropForVoidCall(testedVar, Mode.NullableByValue, ifInst.TrueInst as Block, ifInst);
} else if (arg.MatchLdLoc(out testedVar)) {
TryNullPropForVoidCall(testedVar, Mode.NullableByReference, ifInst.TrueInst as Block, ifInst);
}
}
}
void TryNullPropForVoidCall(ILVariable testedVar, bool testedVarHasReferenceType, Block body, IfInstruction ifInst)
void TryNullPropForVoidCall(ILVariable testedVar, Mode mode, Block body, IfInstruction ifInst)
{
if (body == null || body.Instructions.Count != 1)
return;
@ -143,18 +167,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -143,18 +167,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (bodyInst.MatchNullableRewrap(out var arg)) {
bodyInst = arg;
}
if (!IsValidAccessChain(testedVar, testedVarHasReferenceType, bodyInst, out var varLoad))
if (!IsValidAccessChain(testedVar, mode, bodyInst, out var varLoad))
return;
context.Step("Null-propagation (void call)", body);
context.Step($"Null-propagation (mode={mode}, output=void call)", body);
// if (testedVar != null) { testedVar.AccessChain(); }
// => testedVar?.AccessChain();
IntroduceUnwrap(testedVar, varLoad);
IntroduceUnwrap(testedVar, varLoad, mode);
ifInst.ReplaceWith(new NullableRewrap(
bodyInst
) { ILRange = ifInst.ILRange });
}
bool IsValidAccessChain(ILVariable testedVar, bool testedVarHasReferenceType, ILInstruction inst, out ILInstruction finalLoad)
bool IsValidAccessChain(ILVariable testedVar, Mode mode, ILInstruction inst, out ILInstruction finalLoad)
{
finalLoad = null;
int chainLength = 0;
@ -196,11 +220,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -196,11 +220,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
bool IsValidEndOfChain()
{
if (testedVarHasReferenceType) {
// either reference type (expect: ldloc(testedVar)) or unconstrained generic type (expect: ldloca(testedVar)).
return inst.MatchLdLocRef(testedVar);
} else {
return NullableLiftingTransform.MatchGetValueOrDefault(inst, testedVar);
switch (mode) {
case Mode.ReferenceType:
// either reference type (expect: ldloc(testedVar)) or unconstrained generic type (expect: ldloca(testedVar)).
return inst.MatchLdLocRef(testedVar);
case Mode.NullableByValue:
return NullableLiftingTransform.MatchGetValueOrDefault(inst, testedVar);
case Mode.NullableByReference:
return NullableLiftingTransform.MatchGetValueOrDefault(inst, out ILInstruction arg)
&& arg.MatchLdLoc(testedVar);
default:
throw new ArgumentOutOfRangeException("mode");
}
}
}
@ -210,18 +240,34 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -210,18 +240,34 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return method.AccessorOwner is IProperty p && p.Getter == method;
}
private void IntroduceUnwrap(ILVariable testedVar, ILInstruction varLoad)
private void IntroduceUnwrap(ILVariable testedVar, ILInstruction varLoad, Mode mode)
{
if (NullableLiftingTransform.MatchGetValueOrDefault(varLoad, testedVar)) {
varLoad.ReplaceWith(new NullableUnwrap(
varLoad.ResultType,
new LdLoc(testedVar) { ILRange = varLoad.Children[0].ILRange }
) { ILRange = varLoad.ILRange });
} else {
// Wrap varLoad in nullable.unwrap:
var children = varLoad.Parent.Children;
children[varLoad.ChildIndex] = new NullableUnwrap(varLoad.ResultType, varLoad);
var oldParentChildren = varLoad.Parent.Children;
var oldChildIndex = varLoad.ChildIndex;
ILInstruction replacement;
switch (mode) {
case Mode.ReferenceType:
// Wrap varLoad in nullable.unwrap:
replacement = new NullableUnwrap(varLoad.ResultType, varLoad, refInput: varLoad.ResultType == StackType.Ref);
break;
case Mode.NullableByValue:
Debug.Assert(NullableLiftingTransform.MatchGetValueOrDefault(varLoad, testedVar));
replacement = new NullableUnwrap(
varLoad.ResultType,
new LdLoc(testedVar) { ILRange = varLoad.Children[0].ILRange }
) { ILRange = varLoad.ILRange };
break;
case Mode.NullableByReference:
replacement = new NullableUnwrap(
varLoad.ResultType,
new LdLoc(testedVar) { ILRange = varLoad.Children[0].ILRange },
refInput: true
) { ILRange = varLoad.ILRange };
break;
default:
throw new ArgumentOutOfRangeException("mode");
}
oldParentChildren[oldChildIndex] = replacement;
}
}

Loading…
Cancel
Save