Browse Source

Handle the special cases where the range does not have a start or endpoint.

pull/1986/head
Daniel Grunwald 5 years ago
parent
commit
1926756cfa
  1. 62
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs
  2. 167
      ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs

62
ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs

@ -75,6 +75,26 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{ {
return i; return i;
} }
public static Range[] SeveralRanges()
{
// Some of these are semantically identical, but we can still distinguish them in the IL code:
return new Range[14] {
..,
0..,
^0..,
GetInt(1)..,
^GetInt(2)..,
..0,
..^0,
..GetInt(3),
..^GetInt(4),
0..^0,
^0..0,
0..0,
GetInt(5)..GetInt(6),
0..(GetInt(7) + GetInt(8))
};
}
public static void UseIndex() public static void UseIndex()
{ {
@ -118,12 +138,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
public static void UseRange() public static void UseRange()
{ {
Console.WriteLine(GetArray()[GetRange()]); Console.WriteLine(GetArray()[GetRange()]);
#if TODO
//Console.WriteLine(GetList()[GetRange()]); // fails to compile //Console.WriteLine(GetList()[GetRange()]); // fails to compile
Console.WriteLine(GetSpan()[GetRange()].ToString()); Console.WriteLine(GetSpan()[GetRange()].ToString());
Console.WriteLine(GetString()[GetRange()]); Console.WriteLine(GetString()[GetRange()]);
Console.WriteLine(new CustomList()[GetRange()]); Console.WriteLine(new CustomList()[GetRange()]);
#endif
Console.WriteLine(new CustomList2()[GetRange()]); Console.WriteLine(new CustomList2()[GetRange()]);
} }
public static void UseNewRangeFromIndex() public static void UseNewRangeFromIndex()
@ -175,36 +193,64 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
public static void UseNewRangeFromIntegers_OnlyEndPoint() public static void UseNewRangeFromIntegers_OnlyEndPoint()
{ {
Console.WriteLine(GetArray()[..GetInt(2)]); Console.WriteLine(GetArray()[..GetInt(2)]);
#if TODO
//Console.WriteLine(GetList()[..GetInt()]); // fails to compile //Console.WriteLine(GetList()[..GetInt()]); // fails to compile
Console.WriteLine(GetSpan()[..GetInt(2)].ToString()); Console.WriteLine(GetSpan()[..GetInt(2)].ToString());
Console.WriteLine(GetString()[..GetInt(2)]); Console.WriteLine(GetString()[..GetInt(2)]);
Console.WriteLine(new CustomList()[..GetInt(2)]); Console.WriteLine(new CustomList()[..GetInt(2)]);
#endif
Console.WriteLine(new CustomList2()[..GetInt(2)]); Console.WriteLine(new CustomList2()[..GetInt(2)]);
} }
public static void UseNewRangeFromIntegers_OnlyEndPoint_FromEnd()
{
Console.WriteLine(GetArray()[..^GetInt(2)]);
//Console.WriteLine(GetList()[..^GetInt()]); // fails to compile
Console.WriteLine(GetSpan()[..^GetInt(2)].ToString());
Console.WriteLine(GetString()[..^GetInt(2)]);
Console.WriteLine(new CustomList()[..^GetInt(2)]);
Console.WriteLine(new CustomList2()[..^GetInt(2)]);
}
public static void UseNewRangeFromIntegers_OnlyStartPoint() public static void UseNewRangeFromIntegers_OnlyStartPoint()
{ {
Console.WriteLine(GetArray()[GetInt(1)..]); Console.WriteLine(GetArray()[GetInt(1)..]);
#if TODO
//Console.WriteLine(GetList()[GetInt()..]); // fails to compile //Console.WriteLine(GetList()[GetInt()..]); // fails to compile
Console.WriteLine(GetSpan()[GetInt(1)..].ToString()); Console.WriteLine(GetSpan()[GetInt(1)..].ToString());
Console.WriteLine(GetString()[GetInt(1)..]); Console.WriteLine(GetString()[GetInt(1)..]);
Console.WriteLine(new CustomList()[GetInt(1)..]); Console.WriteLine(new CustomList()[GetInt(1)..]);
#endif
Console.WriteLine(new CustomList2()[GetInt(1)..]); Console.WriteLine(new CustomList2()[GetInt(1)..]);
} }
public static void UseNewRangeFromIntegers_OnlyStartPoint_FromEnd()
{
Console.WriteLine(GetArray()[^GetInt(1)..]);
//Console.WriteLine(GetList()[^GetInt()..]); // fails to compile
Console.WriteLine(GetSpan()[^GetInt(1)..].ToString());
Console.WriteLine(GetString()[^GetInt(1)..]);
Console.WriteLine(new CustomList()[^GetInt(1)..]);
Console.WriteLine(new CustomList2()[^GetInt(1)..]);
}
public static void UseConstantRange()
{
// Fortunately the C# compiler doesn't optimize
// "str.Length - 2 - 1" here, so the normal pattern applies.
Console.WriteLine(GetString()[1..2]);
Console.WriteLine(GetString()[1..^1]);
Console.WriteLine(GetString()[^2..^1]);
Console.WriteLine(GetString()[..1]);
Console.WriteLine(GetString()[..^1]);
Console.WriteLine(GetString()[1..]);
Console.WriteLine(GetString()[^1..]);
}
public static void UseWholeRange() public static void UseWholeRange()
{ {
Console.WriteLine(GetArray()[..]); Console.WriteLine(GetArray()[..]);
#if TODO
//Console.WriteLine(GetList()[..]); // fails to compile //Console.WriteLine(GetList()[..]); // fails to compile
Console.WriteLine(GetSpan()[..].ToString()); Console.WriteLine(GetSpan()[..].ToString());
Console.WriteLine(GetString()[..]); Console.WriteLine(GetString()[..]);
Console.WriteLine(new CustomList()[..]); Console.WriteLine(new CustomList()[..]);
#endif
Console.WriteLine(new CustomList2()[..]); Console.WriteLine(new CustomList2()[..]);
} }

167
ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs

@ -63,7 +63,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (!(bni.Left.MatchLdLen(StackType.I4, out var arrayLoad) && arrayLoad.MatchLdLoc(array))) if (!(bni.Left.MatchLdLen(StackType.I4, out var arrayLoad) && arrayLoad.MatchLdLoc(array)))
return false; return false;
var indexMethods = new IndexMethods(context.TypeSystem); var indexMethods = new IndexMethods(context.TypeSystem);
if (!indexMethods.AllValid) if (!indexMethods.IsValid)
return false; // don't use System.Index if not supported by the target framework return false; // don't use System.Index if not supported by the target framework
context.Step("ldelema indexed from end", ldelema); context.Step("ldelema indexed from end", ldelema);
foreach (var node in bni.Left.Descendants) foreach (var node in bni.Left.Descendants)
@ -84,7 +84,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
public readonly IMethod RangeCtor; public readonly IMethod RangeCtor;
public IType IndexType => IndexCtor?.DeclaringType; public IType IndexType => IndexCtor?.DeclaringType;
public IType RangeType => RangeCtor?.DeclaringType; public IType RangeType => RangeCtor?.DeclaringType;
public bool AllValid => IndexCtor != null && IndexImplicitConv != null && RangeCtor != null; public bool IsValid => IndexCtor != null && IndexImplicitConv != null && RangeCtor != null;
public readonly IMethod RangeStartAt;
public readonly IMethod RangeEndAt;
public readonly IMethod RangeGetAll;
public IndexMethods(ICompilation compilation) public IndexMethods(ICompilation compilation)
{ {
@ -96,7 +100,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
foreach (var op in indexType.GetMethods(m => m.IsOperator && m.Name == "op_Implicit")) { foreach (var op in indexType.GetMethods(m => m.IsOperator && m.Name == "op_Implicit")) {
if (op.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) { if (op.Parameters.Count == 1 && op.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) {
this.IndexImplicitConv = op; this.IndexImplicitConv = op;
} }
} }
@ -107,6 +111,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
this.RangeCtor = ctor; this.RangeCtor = ctor;
} }
} }
foreach (var m in rangeType.GetMethods(m => m.Parameters.Count == 1)) {
if (m.Parameters.Count == 1 && m.Parameters[0].Type.IsKnownType(KnownTypeCode.Index)) {
if (m.Name == "StartAt")
this.RangeStartAt = m;
else if (m.Name == "EndAt")
this.RangeEndAt = m;
}
}
foreach (var p in rangeType.GetProperties(p => p.IsStatic && p.Name == "All")) {
this.RangeGetAll = p.Getter;
}
} }
} }
@ -152,18 +167,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (rangeVar != null) if (rangeVar != null)
return; return;
if (startIndexKind == IndexKind.FromStart) {
// FromStart is only relevant for slicing; indexing from the start does not involve System.Index at all.
return;
}
if (!CheckContainerLengthVariableUseCount(containerLengthVar, startIndexKind)) {
return;
}
// startOffsetVar might be used deep inside a complex statement, ensure we can inline up to that point:
for (int i = startPos; i < pos; i++) {
if (!ILInlining.CanInlineInto(block.Instructions[pos], startOffsetVar, block.Instructions[i]))
return;
}
if (!(startOffsetVar.LoadInstructions.Single().Parent is CallInstruction call)) if (!(startOffsetVar.LoadInstructions.Single().Parent is CallInstruction call))
return; return;
if (call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter && call.Arguments.Count == 2) { if (call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter && call.Arguments.Count == 2) {
@ -176,9 +179,24 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return; return;
if (call.Method.Parameters.Count != 2) if (call.Method.Parameters.Count != 2)
return; return;
} else if (IsSlicingMethod(call.Method)) {
TransformSlicing(sliceLengthWasMisdetectedAsStartOffset: true);
return;
} else { } else {
return; return;
} }
if (startIndexKind == IndexKind.FromStart) {
// FromStart is only relevant for slicing; indexing from the start does not involve System.Index at all.
return;
}
if (!CheckContainerLengthVariableUseCount(containerLengthVar, startIndexKind)) {
return;
}
// startOffsetVar might be used deep inside a complex statement, ensure we can inline up to that point:
for (int i = startPos; i < pos; i++) {
if (!ILInlining.CanInlineInto(block.Instructions[pos], startOffsetVar, block.Instructions[i]))
return;
}
if (!call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) if (!call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32))
return; return;
if (!MatchContainerVar(call.Arguments[0], ref containerVar)) if (!MatchContainerVar(call.Arguments[0], ref containerVar))
@ -186,7 +204,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (!call.Arguments[1].MatchLdLoc(startOffsetVar)) if (!call.Arguments[1].MatchLdLoc(startOffsetVar))
return; return;
var specialMethods = new IndexMethods(context.TypeSystem); var specialMethods = new IndexMethods(context.TypeSystem);
if (!specialMethods.AllValid) if (!specialMethods.IsValid)
return; return;
if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: false)) if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: false))
return; return;
@ -207,16 +225,31 @@ namespace ICSharpCode.Decompiler.IL.Transforms
block.Instructions.RemoveRange(startPos, pos - startPos); block.Instructions.RemoveRange(startPos, pos - startPos);
} }
void TransformSlicing() void TransformSlicing(bool sliceLengthWasMisdetectedAsStartOffset = false)
{ {
// stloc containerLengthVar(call get_Length(ldloc containerVar)) ILVariable sliceLengthVar;
// stloc startOffset(call GetOffset(startIndexLoad, ldloc length)) ILInstruction sliceLengthVarInit;
// -- we are here -- if (sliceLengthWasMisdetectedAsStartOffset) {
// stloc sliceLengthVar(binary.sub.i4(call GetOffset(endIndexLoad, ldloc length), ldloc startOffset)) // Special case: when slicing without a start point, the slice length calculation is mis-detected as the start offset,
// complex_expr(call Slice(ldloc containerVar, ldloc startOffset, ldloc sliceLength)) // and since it only has a single use, we end in TransformIndexing(), which then calls TransformSlicing
if (!block.Instructions[pos].MatchStLoc(out var sliceLengthVar, out var sliceLengthVarInit)) // on this code path.
return; sliceLengthVar = startOffsetVar;
pos++; sliceLengthVarInit = ((StLoc)sliceLengthVar.StoreInstructions.Single()).Value;
startOffsetVar = null;
startIndexLoad = new LdcI4(0);
startIndexKind = IndexKind.TheStart;
} else {
// stloc containerLengthVar(call get_Length(ldloc containerVar))
// stloc startOffset(call GetOffset(startIndexLoad, ldloc length))
// -- we are here --
// stloc sliceLengthVar(binary.sub.i4(call GetOffset(endIndexLoad, ldloc length), ldloc startOffset))
// complex_expr(call Slice(ldloc containerVar, ldloc startOffset, ldloc sliceLength))
if (!block.Instructions[pos].MatchStLoc(out sliceLengthVar, out sliceLengthVarInit))
return;
pos++;
}
if (!(sliceLengthVar.IsSingleDefinition && sliceLengthVar.LoadCount == 1)) if (!(sliceLengthVar.IsSingleDefinition && sliceLengthVar.LoadCount == 1))
return; return;
if (!MatchSliceLength(sliceLengthVarInit, out IndexKind endIndexKind, out ILInstruction endIndexLoad, containerLengthVar, ref containerVar, startOffsetVar)) if (!MatchSliceLength(sliceLengthVarInit, out IndexKind endIndexKind, out ILInstruction endIndexLoad, containerLengthVar, ref containerVar, startOffsetVar))
@ -232,25 +265,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
if (!(sliceLengthVar.LoadInstructions.Single().Parent is CallInstruction call)) if (!(sliceLengthVar.LoadInstructions.Single().Parent is CallInstruction call))
return; return;
if (call.Method.Name == "Slice") { if (!IsSlicingMethod(call.Method))
// OK, custom class slicing
} else if (call.Method.Name == "Substring" && call.Method.DeclaringType.IsKnownType(KnownTypeCode.String)) {
// OK, string slicing
} else {
return;
}
if (call.Method.IsExtensionMethod)
return;
if (call.Method.Parameters.Count != 2)
return;
if (!call.Method.Parameters.All(p => p.Type.IsKnownType(KnownTypeCode.Int32)))
return; return;
if (call.Arguments.Count != 3) if (call.Arguments.Count != 3)
return; return;
if (!MatchContainerVar(call.Arguments[0], ref containerVar)) if (!MatchContainerVar(call.Arguments[0], ref containerVar))
return; return;
if (!call.Arguments[1].MatchLdLoc(startOffsetVar)) if (startOffsetVar == null) {
return; Debug.Assert(startIndexKind == IndexKind.TheStart);
if (!call.Arguments[1].MatchLdcI4(0))
return;
} else {
if (!call.Arguments[1].MatchLdLoc(startOffsetVar))
return;
}
if (!call.Arguments[2].MatchLdLoc(sliceLengthVar)) if (!call.Arguments[2].MatchLdLoc(sliceLengthVar))
return; return;
if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: true)) if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: true))
@ -275,7 +303,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
var specialMethods = new IndexMethods(context.TypeSystem); var specialMethods = new IndexMethods(context.TypeSystem);
if (!specialMethods.AllValid) if (!specialMethods.IsValid)
return; return;
context.Step($"{call.Method.Name} sliced with {startIndexKind}..{endIndexKind}", call); context.Step($"{call.Method.Name} sliced with {startIndexKind}..{endIndexKind}", call);
@ -286,6 +314,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
newCall.Arguments.Add(call.Arguments[0]); newCall.Arguments.Add(call.Arguments[0]);
if (rangeVar != null) { if (rangeVar != null) {
newCall.Arguments.Add(rangeVarInit); newCall.Arguments.Add(rangeVarInit);
} else if (startIndexKind == IndexKind.TheStart && endIndexKind == IndexKind.TheEnd && specialMethods.RangeGetAll != null) {
newCall.Arguments.Add(new Call(specialMethods.RangeGetAll));
} else if (startIndexKind == IndexKind.TheStart && specialMethods.RangeEndAt != null) {
var rangeCtorCall = new Call(specialMethods.RangeEndAt);
rangeCtorCall.Arguments.Add(MakeIndex(endIndexKind, endIndexLoad, specialMethods));
newCall.Arguments.Add(rangeCtorCall);
} else if (endIndexKind == IndexKind.TheEnd && specialMethods.RangeStartAt != null) {
var rangeCtorCall = new Call(specialMethods.RangeStartAt);
rangeCtorCall.Arguments.Add(MakeIndex(startIndexKind, startIndexLoad, specialMethods));
newCall.Arguments.Add(rangeCtorCall);
} else { } else {
var rangeCtorCall = new NewObj(specialMethods.RangeCtor); var rangeCtorCall = new NewObj(specialMethods.RangeCtor);
rangeCtorCall.Arguments.Add(MakeIndex(startIndexKind, startIndexLoad, specialMethods)); rangeCtorCall.Arguments.Add(MakeIndex(startIndexKind, startIndexLoad, specialMethods));
@ -301,15 +339,27 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
static bool IsSlicingMethod(IMethod method)
{
if (method.IsExtensionMethod)
return false;
if (method.Parameters.Count != 2)
return false;
if (!method.Parameters.All(p => p.Type.IsKnownType(KnownTypeCode.Int32)))
return false;
return method.Name == "Slice"
|| (method.Name == "Substring" && method.DeclaringType.IsKnownType(KnownTypeCode.String));
}
/// <summary> /// <summary>
/// Check that the number of uses of the containerLengthVar variable matches those expected in the pattern. /// Check that the number of uses of the containerLengthVar variable matches those expected in the pattern.
/// </summary> /// </summary>
private bool CheckContainerLengthVariableUseCount(ILVariable containerLengthVar, IndexKind startIndexKind, IndexKind endIndexKind = IndexKind.FromStart) private bool CheckContainerLengthVariableUseCount(ILVariable containerLengthVar, IndexKind startIndexKind, IndexKind endIndexKind = IndexKind.FromStart)
{ {
int expectedUses = 0; int expectedUses = 0;
if (startIndexKind != IndexKind.FromStart) if (startIndexKind != IndexKind.FromStart && startIndexKind != IndexKind.TheStart)
expectedUses += 1; expectedUses += 1;
if (endIndexKind != IndexKind.FromStart) if (endIndexKind != IndexKind.FromStart && endIndexKind != IndexKind.TheStart)
expectedUses += 1; expectedUses += 1;
if (containerLengthVar != null) { if (containerLengthVar != null) {
return containerLengthVar.LoadCount == expectedUses; return containerLengthVar.LoadCount == expectedUses;
@ -349,14 +399,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
// --> // -->
// complex_expr(call get_Item(ldloc container, ldobj startIndexLoad)) // complex_expr(call get_Item(ldloc container, ldobj startIndexLoad))
return new LdObj(indexLoad, specialMethods.IndexType); return new LdObj(indexLoad, specialMethods.IndexType);
} else if (indexKind == IndexKind.FromEnd) { } else if (indexKind == IndexKind.FromEnd || indexKind == IndexKind.TheEnd) {
// stloc offsetVar(binary.sub.i4(call get_Length/get_Count(ldloc container), startIndexLoad)) // stloc offsetVar(binary.sub.i4(call get_Length/get_Count(ldloc container), startIndexLoad))
// complex_expr(call get_Item(ldloc container, ldloc startOffsetVar)) // complex_expr(call get_Item(ldloc container, ldloc startOffsetVar))
// --> // -->
// complex_expr(call get_Item(ldloc container, newobj System.Index(startIndexLoad, fromEnd: true))) // complex_expr(call get_Item(ldloc container, newobj System.Index(startIndexLoad, fromEnd: true)))
return new NewObj(specialMethods.IndexCtor) { Arguments = { indexLoad, new LdcI4(1) } }; return new NewObj(specialMethods.IndexCtor) { Arguments = { indexLoad, new LdcI4(1) } };
} else { } else {
Debug.Assert(indexKind == IndexKind.FromStart); Debug.Assert(indexKind == IndexKind.FromStart || indexKind == IndexKind.TheStart);
return new Call(specialMethods.IndexImplicitConv) { Arguments = { indexLoad } }; return new Call(specialMethods.IndexImplicitConv) { Arguments = { indexLoad } };
} }
} }
@ -462,7 +512,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// <summary> /// <summary>
/// indexLoad is an integer, from the end of the container /// indexLoad is an integer, from the end of the container
/// </summary> /// </summary>
FromEnd FromEnd,
/// <summary>
/// Always equivalent to `0`, used for the start-index when slicing without a startpoint `a[..end]`
/// </summary>
TheStart,
/// <summary>
/// Always equivalent to `^0`, used for the end-index when slicing without an endpoint `a[start..]`
/// </summary>
TheEnd,
} }
/// <summary> /// <summary>
@ -477,7 +535,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms
ILVariable containerLengthVar, ref ILVariable containerVar) ILVariable containerLengthVar, ref ILVariable containerVar)
{ {
indexLoad = inst; indexLoad = inst;
if (inst is CallInstruction call) { if (MatchContainerLength(inst, containerLengthVar, ref containerVar)) {
indexLoad = new LdcI4(0);
return IndexKind.TheEnd;
} else if (inst is CallInstruction call) {
// call System.Index.GetOffset(indexLoad, ldloc containerLengthVar) // call System.Index.GetOffset(indexLoad, ldloc containerLengthVar)
if (call.Method.Name != "GetOffset") if (call.Method.Name != "GetOffset")
return IndexKind.FromStart; return IndexKind.FromStart;
@ -513,10 +574,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (inst is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) { if (inst is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) {
if (bni.CheckForOverflow || bni.ResultType != StackType.I4 || bni.IsLifted) if (bni.CheckForOverflow || bni.ResultType != StackType.I4 || bni.IsLifted)
return false; return false;
if (!bni.Right.MatchLdLoc(startOffsetVar)) if (startOffsetVar == null) {
return false; // When slicing without explicit start point: `a[..endIndex]`
if (!bni.Right.MatchLdcI4(0))
return false;
} else {
if (!bni.Right.MatchLdLoc(startOffsetVar))
return false;
}
endIndexKind = MatchGetOffset(bni.Left, out endIndexLoad, containerLengthVar, ref containerVar); endIndexKind = MatchGetOffset(bni.Left, out endIndexLoad, containerLengthVar, ref containerVar);
return true; return true;
} else if (startOffsetVar == null) {
// When slicing without explicit start point: `a[..endIndex]`, the compiler doesn't always emit the "- 0".
endIndexKind = MatchGetOffset(inst, out endIndexLoad, containerLengthVar, ref containerVar);
return true;
} else { } else {
return false; return false;
} }

Loading…
Cancel
Save