diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Errors/UserStringBuilder.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Errors/UserStringBuilder.cs index 5ed7c07341e6..ffdd821a859d 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Errors/UserStringBuilder.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Errors/UserStringBuilder.cs @@ -656,21 +656,20 @@ private void ErrAppendType(CType pType, SubstContext pctx, bool fArgs) // Add [] with (rank-1) commas inside ErrAppendChar('['); -#if ! CSEE // known rank. - if (rank > 1) + if (rank == 1) { - ErrAppendChar('*'); + if (!elementType.AsArrayType().IsSZArray) + { + ErrAppendChar('*'); + } } -#endif - - for (int i = rank; i > 1; --i) + else { - ErrAppendChar(','); -#if ! CSEE - - ErrAppendChar('*'); -#endif + for (int i = rank; i > 1; --i) + { + ErrAppendChar(','); + } } ErrAppendChar(']'); diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Conversions.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Conversions.cs index 5ffdf2ecd269..dfa5710c8271 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Conversions.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Conversions.cs @@ -120,14 +120,16 @@ public static bool FExpRefConv(SymbolLoader loader, CType typeSrc, CType typeDst // o An explicit reference conversion exists from SE to TE. if (typeSrc.IsArrayType() && typeDst.IsArrayType()) { - return typeSrc.AsArrayType().rank == typeDst.AsArrayType().rank && FExpRefConv(loader, typeSrc.AsArrayType().GetElementType(), typeDst.AsArrayType().GetElementType()); + return typeSrc.AsArrayType().rank == typeDst.AsArrayType().rank + && typeSrc.AsArrayType().IsSZArray == typeDst.AsArrayType().IsSZArray + && FExpRefConv(loader, typeSrc.AsArrayType().GetElementType(), typeDst.AsArrayType().GetElementType()); } // * From a one-dimensional array-type S[] to System.Collections.Generic.IList, System.Collections.Generic.IReadOnlyList // and their base interfaces, provided there is an explicit reference conversion from S to T. if (typeSrc.IsArrayType()) { - if (typeSrc.AsArrayType().rank != 1 || + if (!typeSrc.AsArrayType().IsSZArray || !typeDst.isInterfaceType() || typeDst.AsAggregateType().GetTypeArgsAll().Count != 1) { return false; @@ -161,7 +163,7 @@ public static bool FExpRefConv(SymbolLoader loader, CType typeSrc, CType typeDst // are the same type or there is an implicit or explicit reference conversion from S to T. ArrayType arrayDest = typeDst.AsArrayType(); AggregateType aggtypeSrc = typeSrc.AsAggregateType(); - if (arrayDest.rank != 1 || !typeSrc.isInterfaceType() || + if (!arrayDest.IsSZArray || !typeSrc.isInterfaceType() || aggtypeSrc.GetTypeArgsAll().Count != 1) { return false; diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/ExplicitConversion.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/ExplicitConversion.cs index 489449734d30..962e09b908ad 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/ExplicitConversion.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/ExplicitConversion.cs @@ -234,7 +234,7 @@ private bool bindExplicitConversionFromArrayToIList() Debug.Assert(_typeSrc != null); Debug.Assert(_typeDest != null); - if (!_typeSrc.IsArrayType() || _typeSrc.AsArrayType().rank != 1 || + if (!_typeSrc.IsArrayType() || !_typeSrc.AsArrayType().IsSZArray || !_typeDest.isInterfaceType() || _typeDest.AsAggregateType().GetTypeArgsAll().Count != 1) { return false; @@ -319,7 +319,7 @@ private bool bindExplicitConversionFromIListToArray(ArrayType arrayDest) // S[] to System.Collections.Generic.IList or System.Collections.Generic.IReadOnlyList. This is precisely when either S and T // are the same type or there is an implicit or explicit reference conversion from S to T. - if (arrayDest.rank != 1 || !_typeSrc.isInterfaceType() || + if (!arrayDest.IsSZArray || !_typeSrc.isInterfaceType() || _typeSrc.AsAggregateType().GetTypeArgsAll().Count != 1) { return false; @@ -363,7 +363,7 @@ private bool bindExplicitConversionFromArrayToArray(ArrayType arraySrc, ArrayTyp // // * An explicit reference conversion exists from SE to TE. - if (arraySrc.rank != arrayDest.rank) + if (arraySrc.rank != arrayDest.rank || arraySrc.IsSZArray != arrayDest.IsSZArray) { return false; // Ranks do not match. } diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/ExpressionBinder.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/ExpressionBinder.cs index 540da1c269a8..51f7ce641302 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/ExpressionBinder.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/ExpressionBinder.cs @@ -565,7 +565,7 @@ private ExprQuestionMark BindPtrToArray(ExprLocal exprLoc, Expr array) Expr nullTest = GetExprFactory().CreateBinop(ExpressionKind.EK_NE, GetReqPDT(PredefinedType.PT_BOOL), save, GetExprFactory().CreateConstant(wrapArray.Type, ConstVal.Get(0))); Expr lenTest; - if (array.Type.AsArrayType().rank == 1) + if (array.Type.AsArrayType().IsSZArray) { Expr len = GetExprFactory().CreateArrayLength(wrapArray); lenTest = GetExprFactory().CreateBinop(ExpressionKind.EK_NE, GetReqPDT(PredefinedType.PT_BOOL), len, GetExprFactory().CreateConstant(GetReqPDT(PredefinedType.PT_INT), ConstVal.Get(0))); @@ -2069,7 +2069,7 @@ private void AdjustCallArgumentsForParams(CType callingObjectType, CType type, M // we need to create an array and put it as the last arg... CType substitutedArrayType = GetTypes().SubstType(mp.Params[mp.Params.Count - 1], type, pTypeArgs); - if (!substitutedArrayType.IsArrayType() || substitutedArrayType.AsArrayType().rank != 1) + if (!substitutedArrayType.IsArrayType() || !substitutedArrayType.AsArrayType().IsSZArray) { // Invalid type for params array parameter. Happens in LAF scenarios, e.g. // diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/MethodTypeInferrer.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/MethodTypeInferrer.cs index 3a64bbcbb906..f11309402dcf 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/MethodTypeInferrer.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/MethodTypeInferrer.cs @@ -1185,7 +1185,7 @@ private bool ExactArrayInference(CType pSource, CType pDest) } ArrayType pArraySource = pSource.AsArrayType(); ArrayType pArrayDest = pDest.AsArrayType(); - if (pArraySource.rank != pArrayDest.rank) + if (pArraySource.rank != pArrayDest.rank || pArraySource.IsSZArray != pArrayDest.IsSZArray) { return false; } @@ -1379,7 +1379,7 @@ private bool LowerBoundArrayInference(CType pSource, CType pDest) if (pDest.IsArrayType()) { ArrayType pArrayDest = pDest.AsArrayType(); - if (pArrayDest.rank != pArraySource.rank) + if (pArrayDest.rank != pArraySource.rank || pArrayDest.IsSZArray != pArraySource.IsSZArray) { return false; } @@ -1391,7 +1391,7 @@ private bool LowerBoundArrayInference(CType pSource, CType pDest) pDest.isPredefType(PredefinedType.PT_G_IREADONLYCOLLECTION) || pDest.isPredefType(PredefinedType.PT_G_IREADONLYLIST)) { - if (pArraySource.rank != 1) + if (!pArraySource.IsSZArray) { return false; } @@ -1727,7 +1727,7 @@ private bool UpperBoundArrayInference(CType pSource, CType pDest) if (pSource.IsArrayType()) { ArrayType pArraySource = pSource.AsArrayType(); - if (pArrayDest.rank != pArraySource.rank) + if (pArrayDest.rank != pArraySource.rank || pArrayDest.IsSZArray != pArraySource.IsSZArray) { return false; } @@ -1739,7 +1739,7 @@ private bool UpperBoundArrayInference(CType pSource, CType pDest) pSource.isPredefType(PredefinedType.PT_G_IREADONLYLIST) || pSource.isPredefType(PredefinedType.PT_G_IREADONLYCOLLECTION)) { - if (pArrayDest.rank != 1) + if (!pArrayDest.IsSZArray) { return false; } diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/PredefinedMembers.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/PredefinedMembers.cs index 8e103b8ca389..8a1441914b7d 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/PredefinedMembers.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/PredefinedMembers.cs @@ -444,7 +444,7 @@ private CType LoadTypeFromSignature(int[] signature, ref int indexIntoSignatures { return null; } - return GetTypeManager().GetArray(elementType, 1); + return GetTypeManager().GetArray(elementType, 1, true); } case MethodSignatureEnum.SIG_METH_TYVAR: { diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Symbols/SymbolLoader.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Symbols/SymbolLoader.cs index df2a4c9ae91a..6c3c7d6fbede 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Symbols/SymbolLoader.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Symbols/SymbolLoader.cs @@ -276,7 +276,7 @@ private bool HasCovariantArrayConversion(ArrayType pSource, ArrayType pDest) // * S and T differ only in element type. In other words, S and T have the same number of dimensions. // * Both SE and TE are reference types. // * An implicit reference conversion exists from SE to TE. - return (pSource.rank == pDest.rank) && + return (pSource.rank == pDest.rank) && pSource.IsSZArray == pDest.IsSZArray && HasImplicitReferenceConversion(pSource.GetElementType(), pDest.GetElementType()); } @@ -301,7 +301,7 @@ private bool HasArrayConversionToInterface(ArrayType pSource, CType pDest) { Debug.Assert(pSource != null); Debug.Assert(pDest != null); - if (pSource.rank != 1) + if (!pSource.IsSZArray) { return false; } diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Tree/Visitors/ExpressionTreeRewriter.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Tree/Visitors/ExpressionTreeRewriter.cs index 77ef6db2e4d6..c7730f46dbf1 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Tree/Visitors/ExpressionTreeRewriter.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Tree/Visitors/ExpressionTreeRewriter.cs @@ -1174,7 +1174,7 @@ private ExprArrayInit GenerateParamsArray(Expr args, PredefinedType pt) { int parameterCount = ExpressionIterator.Count(args); AggregateType paramsArrayElementType = GetSymbolLoader().GetOptPredefTypeErr(pt, true); - ArrayType paramsArrayType = GetSymbolLoader().GetTypeManager().GetArray(paramsArrayElementType, 1); + ArrayType paramsArrayType = GetSymbolLoader().GetTypeManager().GetArray(paramsArrayElementType, 1, true); ExprConstant paramsArrayArg = GetExprFactory().CreateIntegerConstant(parameterCount); ExprArrayInit arrayInit = GetExprFactory().CreateArrayInit(EXPRFLAG.EXF_CANTBENULL, paramsArrayType, args, paramsArrayArg, null); arrayInit.DimensionSize = parameterCount; @@ -1204,7 +1204,7 @@ private ExprArrayInit GenerateMembersArray(AggregateType anonymousType, Predefin } AggregateType paramsArrayElementType = GetSymbolLoader().GetOptPredefTypeErr(pt, true); - ArrayType paramsArrayType = GetSymbolLoader().GetTypeManager().GetArray(paramsArrayElementType, 1); + ArrayType paramsArrayType = GetSymbolLoader().GetTypeManager().GetArray(paramsArrayElementType, 1, true); ExprConstant paramsArrayArg = GetExprFactory().CreateIntegerConstant(methodCount); ExprArrayInit arrayInit = GetExprFactory().CreateArrayInit(EXPRFLAG.EXF_CANTBENULL, paramsArrayType, newArgs, paramsArrayArg, null); arrayInit.DimensionSize = methodCount; diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/ArrayType.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/ArrayType.cs index 9e4d5a14ac2f..3ac34871261d 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/ArrayType.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/ArrayType.cs @@ -13,6 +13,8 @@ internal sealed class ArrayType : CType // rank of the array. zero means unknown rank int [?]. public int rank; + public bool IsSZArray { get; set; } + public CType GetElementType() { return _pElementType; } public void SetElementType(CType pType) { _pElementType = pType; } diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/Type.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/Type.cs index d5c93cb81b2e..edc0c23844f2 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/Type.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/Type.cs @@ -97,14 +97,7 @@ private static Type CalculateAssociatedSystemType(CType src) case TypeKind.TK_ArrayType: ArrayType a = src.AsArrayType(); Type elementType = a.GetElementType().AssociatedSystemType; - if (a.rank == 1) - { - result = elementType.MakeArrayType(); - } - else - { - result = elementType.MakeArrayType(a.rank); - } + result = a.IsSZArray ? elementType.MakeArrayType() : elementType.MakeArrayType(a.rank); break; case TypeKind.TK_NullableType: diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/TypeFactory.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/TypeFactory.cs index c28e52831ab6..62793a59c8fe 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/TypeFactory.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/TypeFactory.cs @@ -112,12 +112,13 @@ public ErrorType CreateError( } // Derived types - parent is base type - public ArrayType CreateArray(Name name, CType pElementType, int rank) + public ArrayType CreateArray(Name name, CType pElementType, int rank, bool isSZArray) { ArrayType type = new ArrayType(); type.SetName(name); type.rank = rank; + type.IsSZArray = isSZArray; type.SetElementType(pElementType); type.SetTypeKind(TypeKind.TK_ArrayType); diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/TypeManager.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/TypeManager.cs index 2f14806d0a68..7e0322afeca9 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/TypeManager.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Types/TypeManager.cs @@ -169,15 +169,24 @@ public TypeParameterType GetTypeVarSym(int iv, TypeManager pTypeManager, bool fM } } - public ArrayType GetArray(CType elementType, int args) + public ArrayType GetArray(CType elementType, int args, bool isSZArray) { Name name; Debug.Assert(args > 0 && args < 32767); + Debug.Assert(args == 1 || !isSZArray); switch (args) { case 1: + if (isSZArray) + { + goto case 2; + } + else + { + goto default; + } case 2: name = NameManager.GetPredefinedName(PredefinedName.PN_ARRAY0 + args); break; @@ -191,7 +200,7 @@ public ArrayType GetArray(CType elementType, int args) if (pArray == null) { // No existing array symbol. Create a new one. - pArray = _typeFactory.CreateArray(name, elementType, args); + pArray = _typeFactory.CreateArray(name, elementType, args, isSZArray); pArray.InitFromParent(); _typeTable.InsertArray(name, elementType, pArray); @@ -567,7 +576,7 @@ private CType SubstTypeCore(CType type, SubstContext pctx) case TypeKind.TK_ArrayType: typeDst = SubstTypeCore(typeSrc = type.AsArrayType().GetElementType(), pctx); - return (typeDst == typeSrc) ? type : GetArray(typeDst, type.AsArrayType().rank); + return (typeDst == typeSrc) ? type : GetArray(typeDst, type.AsArrayType().rank, type.AsArrayType().IsSZArray); case TypeKind.TK_PointerType: typeDst = SubstTypeCore(typeSrc = type.AsPointerType().GetReferentType(), pctx); @@ -703,7 +712,7 @@ private bool SubstEqualTypesCore(CType typeDst, CType typeSrc, SubstContext pctx return false; case TypeKind.TK_ArrayType: - if (typeDst.GetTypeKind() != TypeKind.TK_ArrayType || typeDst.AsArrayType().rank != typeSrc.AsArrayType().rank) + if (typeDst.GetTypeKind() != TypeKind.TK_ArrayType || typeDst.AsArrayType().rank != typeSrc.AsArrayType().rank || typeDst.AsArrayType().IsSZArray != typeSrc.AsArrayType().IsSZArray) return false; goto LCheckBases; @@ -1282,7 +1291,7 @@ private bool TryArrayVarianceAdjustmentToGetAccessibleType(CSemanticChecker sema CType intermediateType; if (GetBestAccessibleType(semanticChecker, bindingContext, elementType, out intermediateType)) { - typeDst = this.GetArray(intermediateType, typeSrc.rank); + typeDst = this.GetArray(intermediateType, typeSrc.rank, typeSrc.IsSZArray); Debug.Assert(semanticChecker.CheckTypeAccess(typeDst, bindingContext.ContextForMemberLookup())); return true; diff --git a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/SymbolTable.cs b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/SymbolTable.cs index 7875628434f6..5064f3c1075d 100644 --- a/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/SymbolTable.cs +++ b/src/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/SymbolTable.cs @@ -819,7 +819,15 @@ private CType ProcessSpecialTypeInChain(NamespaceOrAggregateSymbol parent, Type else if (t.IsArray) { // Now we return an array of nesting level corresponding to the rank. - ctype = _typeManager.GetArray(GetCTypeFromType(t.GetElementType()), t.GetArrayRank()); + ctype = _typeManager.GetArray( + GetCTypeFromType(t.GetElementType()), + t.GetArrayRank(), +#if netcoreapp + t.IsSZArray +#else + t.GetElementType().MakeArrayType() == t +#endif + ); return ctype; } else if (t.IsPointer) diff --git a/src/Microsoft.CSharp/tests/ArrayHandling.cs b/src/Microsoft.CSharp/tests/ArrayHandling.cs new file mode 100644 index 000000000000..057ab09f725d --- /dev/null +++ b/src/Microsoft.CSharp/tests/ArrayHandling.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.CSharp.RuntimeBinder.Tests +{ + public class ArrayHandling + { + [Fact] + public void SingleRankNonSZArray() + { + dynamic d = Array.CreateInstance(typeof(int), new[] { 8 }, new[] { -2 }); + d.SetValue(32, 3); + d.SetValue(28, -1); + Assert.Equal(32, d.GetValue(3)); + Assert.Equal(28, d.GetValue(-1)); + } + + [Fact] + public void SingleRankNonSZArrayIndexed() + { + dynamic d = Array.CreateInstance(typeof(int), new[] { 8 }, new[] { -2 }); + d[3] = 32; + d[-1] = 28; + Assert.Equal(32, d[3]); + Assert.Equal(28, d[-1]); + } + + [Fact] + public void ArrayTypeNames() + { + dynamic d = Array.CreateInstance(typeof(int), new[] { 8 }, new[] { -2 }); + RuntimeBinderException ex = Assert.Throws(() => { string s = d; }); + Assert.Contains("int[*]", ex.Message); + + d = new int[3]; + ex = Assert.Throws(() => { string s = d; }); + Assert.Contains("int[]", ex.Message); + + d = new int[3, 2, 1]; + ex = Assert.Throws(() => { string s = d; }); + Assert.Contains("int[,,]", ex.Message); + + d = Array.CreateInstance(typeof(int), new[] { 3, 2, 1 }, new[] { -2, 2, -0 }); + ex = Assert.Throws(() => { string s = d; }); + Assert.Contains("int[,,]", ex.Message); + + } + } +} diff --git a/src/Microsoft.CSharp/tests/Microsoft.CSharp.Tests.csproj b/src/Microsoft.CSharp/tests/Microsoft.CSharp.Tests.csproj index a0a1b70cfa69..e6487e881efa 100644 --- a/src/Microsoft.CSharp/tests/Microsoft.CSharp.Tests.csproj +++ b/src/Microsoft.CSharp/tests/Microsoft.CSharp.Tests.csproj @@ -10,6 +10,7 @@ Common\System\Runtime\Serialization\Formatters\BinaryFormatterHelpers.cs + @@ -18,4 +19,4 @@ - + \ No newline at end of file