Skip to content

Commit 4879186

Browse files
committed
sg: jagged arrays
1 parent 310dcb6 commit 4879186

File tree

10 files changed

+1219
-57
lines changed

10 files changed

+1219
-57
lines changed

src/FastCloner.SourceGenerator/CloneCodeGenerator.cs

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -538,13 +538,32 @@ private void WriteClonerClass()
538538
sb.AppendLine(" /// Helper class for cloning generic types.");
539539
sb.AppendLine(" /// </summary>");
540540

541+
// Determine the type parameter name and string
542+
// If parent type has type parameters, we use the first one as the Cloner's parameter
543+
// (since they're in scope within the extension class)
544+
// If parent has no type parameters, we need to define our own 'T'
541545
var typeParams = GetTypeParametersString();
542546
var constraints = GetTypeConstraintsString();
543547

544-
sb.AppendLine($" private static class Cloner{typeParams}{constraints}");
545-
sb.AppendLine(" {");
546-
547-
sb.AppendLine(" public static T Clone(T source, object? state)");
548+
// For non-generic types, Cloner needs its own T parameter
549+
// For generic types, reuse the parent's type parameters
550+
var typeParamsArray = _context.Model.TypeParameters.GetArray();
551+
if (typeParamsArray == null || typeParamsArray.Length == 0)
552+
{
553+
sb.AppendLine($" private static class Cloner<T>");
554+
sb.AppendLine(" {");
555+
sb.AppendLine(" public static T Clone(T source, object? state)");
556+
}
557+
else
558+
{
559+
// Use parent's type parameters
560+
sb.AppendLine($" private static class Cloner{typeParams}{constraints}");
561+
sb.AppendLine(" {");
562+
// Use the first type parameter as the clone target type
563+
var firstTypeParam = typeParamsArray[0];
564+
sb.AppendLine($" public static {firstTypeParam} Clone({firstTypeParam} source, object? state)");
565+
}
566+
548567
sb.AppendLine(" {");
549568
sb.AppendLine(" if (source == null) return default;");
550569

@@ -568,15 +587,20 @@ private void WriteClonerClass()
568587

569588
var argType = usage.ArgumentTypeMetadataName;
570589

590+
// Use the appropriate type parameter for cast operations
591+
var castTypeParam = (typeParamsArray == null || typeParamsArray.Length == 0)
592+
? "T"
593+
: typeParamsArray[0];
594+
571595
if (usage.IsSafe)
572596
{
573-
sb.AppendLine($" if (typeof(T) == typeof({argType})) return (T)(object)source;");
597+
sb.AppendLine($" if (typeof({castTypeParam}) == typeof({argType})) return ({castTypeParam})(object)source;");
574598
}
575599
else if (usage.IsClonable && !string.IsNullOrEmpty(usage.ExtensionClassFQN))
576600
{
577601
// Generate dispatch to concrete FastDeepClone
578-
sb.AppendLine($" if (typeof(T) == typeof({argType}))");
579-
sb.AppendLine($" return (T)(object)({usage.ExtensionClassFQN}.FastDeepClone(({argType})(object)source)!);");
602+
sb.AppendLine($" if (typeof({castTypeParam}) == typeof({argType}))");
603+
sb.AppendLine($" return ({castTypeParam})(object)({usage.ExtensionClassFQN}.FastDeepClone(({argType})(object)source)!);");
580604
}
581605
else if (usage.CollectionModel != null)
582606
{
@@ -591,8 +615,8 @@ private void WriteClonerClass()
591615
? $"(({argType})(object)source, state as FcGeneratedCloneState)"
592616
: $"(({argType})(object)source)";
593617

594-
sb.AppendLine($" if (typeof(T) == typeof({argType}))");
595-
sb.AppendLine($" return (T)(object){helperName}{typeParams}{callArgs};");
618+
sb.AppendLine($" if (typeof({castTypeParam}) == typeof({argType}))");
619+
sb.AppendLine($" return ({castTypeParam})(object){helperName}{typeParams}{callArgs};");
596620
}
597621
else
598622
{
@@ -606,16 +630,21 @@ private void WriteClonerClass()
606630
? $"(({argType})(object)source, state as FcGeneratedCloneState)"
607631
: $"(({argType})(object)source)";
608632

609-
sb.AppendLine($" if (typeof(T) == typeof({argType}))");
610-
sb.AppendLine($" return (T)(object){helperName}{typeParams}{callArgs};");
633+
sb.AppendLine($" if (typeof({castTypeParam}) == typeof({argType}))");
634+
sb.AppendLine($" return ({castTypeParam})(object){helperName}{typeParams}{callArgs};");
611635
}
612636
}
613637
}
614638

639+
// Use the appropriate type parameter for the fallback cast
640+
var fallbackCastTypeParam = (typeParamsArray == null || typeParamsArray.Length == 0)
641+
? "T"
642+
: typeParamsArray[0];
643+
615644
if (_context.IsFastClonerAvailable)
616645
{
617646
sb.AppendLine(" // If FastCloner is available, delegate to it for deep cloning");
618-
sb.AppendLine(" return (T)FastCloner.DeepClone(source);");
647+
sb.AppendLine($" return ({fallbackCastTypeParam})FastCloner.DeepClone(source);");
619648
}
620649
else
621650
{

src/FastCloner.SourceGenerator/CollectionHelperGenerator.cs

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Linq;
23

34
namespace FastCloner.SourceGenerator;
45

@@ -32,6 +33,10 @@ public static void GenerateHelpers(CloneGeneratorContext context)
3233
WriteArrayCloneMethod(context, member);
3334
break;
3435

36+
case MemberTypeKind.MultiDimArray:
37+
WriteMultiDimArrayCloneMethod(context, member);
38+
break;
39+
3540
case MemberTypeKind.Collection:
3641
WriteCollectionCloneMethod(context, member);
3742
break;
@@ -698,8 +703,11 @@ private static void WriteArrayCloneMethod(CloneGeneratorContext context, MemberM
698703
sb.AppendLine();
699704
}
700705

701-
// Arrays use Length, not Count
702-
sb.AppendLine($" var result = new {member.ElementTypeName}[source.Length];");
706+
// Create array - handle jagged arrays properly
707+
// For jagged arrays like int[][], the element type is int[]
708+
// We need to create: new int[source.Length][] (not new int[][source.Length])
709+
var arrayCreationExpr = GetArrayCreationExpression(member.ElementTypeName, "source.Length");
710+
sb.AppendLine($" var result = {arrayCreationExpr};");
703711

704712
if (needsState)
705713
{
@@ -754,6 +762,151 @@ private static void WriteArrayCloneMethod(CloneGeneratorContext context, MemberM
754762
sb.AppendLine();
755763
}
756764

765+
private static void WriteMultiDimArrayCloneMethod(CloneGeneratorContext context, MemberModel member)
766+
{
767+
if (member.ElementTypeName == null) return;
768+
769+
var typeName = member.TypeFullName;
770+
var methodName = context.GetMethodName(typeName);
771+
var isSafe = member.ElementIsSafe;
772+
var hasClonableAttr = member.ElementHasClonableAttr;
773+
var needsState = MemberCloneGenerator.MemberNeedsCircularRefTracking(context, member);
774+
var rank = member.ArrayRank;
775+
var sb = context.Source;
776+
777+
if (needsState)
778+
{
779+
context.NeedsStateClass = true;
780+
}
781+
782+
// Arrays are always reference types
783+
WriteHelperMethodSignature(context, typeName, methodName, needsState, false);
784+
sb.AppendLine(" {");
785+
sb.AppendLine(" if (source == null) return null;");
786+
787+
if (needsState)
788+
{
789+
sb.AppendLine(" if (state != null)");
790+
sb.AppendLine(" {");
791+
sb.AppendLine(" var known = state.GetKnownRef(source);");
792+
sb.AppendLine($" if (known != null) return ({typeName})known;");
793+
sb.AppendLine(" }");
794+
sb.AppendLine();
795+
}
796+
797+
// Generate dimension length variables: len0, len1, len2, ...
798+
for (int d = 0; d < rank; d++)
799+
{
800+
sb.AppendLine($" int len{d} = source.GetLength({d});");
801+
}
802+
sb.AppendLine();
803+
804+
// Create the result array with the same dimensions
805+
// e.g., new T[len0, len1, len2]
806+
var dimList = string.Join(", ", Enumerable.Range(0, rank).Select(d => $"len{d}"));
807+
sb.AppendLine($" var result = new {member.ElementTypeName}[{dimList}];");
808+
809+
if (needsState)
810+
{
811+
sb.AppendLine(" state?.AddKnownRef(source, result);");
812+
}
813+
sb.AppendLine();
814+
815+
// Optimization: if element type is safe, use Array.Copy
816+
if (isSafe)
817+
{
818+
sb.AppendLine(" // Element type is safe, use fast Array.Copy");
819+
sb.AppendLine(" global::System.Array.Copy(source, result, source.Length);");
820+
}
821+
else
822+
{
823+
// Generate nested loops for each dimension
824+
// for (int i0 = 0; i0 < len0; i0++)
825+
// for (int i1 = 0; i1 < len1; i1++)
826+
// ...
827+
// result[i0, i1, ...] = Clone(source[i0, i1, ...]);
828+
829+
for (int d = 0; d < rank; d++)
830+
{
831+
var indent = new string(' ', 12 + d * 4);
832+
sb.AppendLine($"{indent}for (int i{d} = 0; i{d} < len{d}; i{d}++)");
833+
}
834+
835+
var innerIndent = new string(' ', 12 + rank * 4);
836+
var indexList = string.Join(", ", Enumerable.Range(0, rank).Select(d => $"i{d}"));
837+
838+
// Determine how to clone each element
839+
string itemExpr;
840+
if (hasClonableAttr)
841+
{
842+
itemExpr = $"source[{indexList}]?.FastDeepClone()";
843+
}
844+
else if (context.TryGetMemberModel(member.ElementTypeName!, out var nestedModel))
845+
{
846+
var helperName = context.GetOrCreateHelperMethodName(nestedModel);
847+
var elementNeedsState = MemberCloneGenerator.MemberNeedsCircularRefTracking(context, nestedModel);
848+
var actualStateVar = needsState ? "state" : "null";
849+
itemExpr = GetHelperMethodCall(context, helperName, $"source[{indexList}]", elementNeedsState, actualStateVar);
850+
}
851+
else if (context.TryGetImplicitTypeModel(member.ElementTypeName!, out var implicitModel))
852+
{
853+
var helperName = context.GetOrCreateHelperMethodName(implicitModel.FullyQualifiedName);
854+
var elementNeedsState = implicitModel.CanHaveCircularReferences && context.CanHaveCircularReferences;
855+
var actualStateVar = needsState ? "state" : "null";
856+
itemExpr = GetHelperMethodCall(context, helperName, $"source[{indexList}]", elementNeedsState, actualStateVar);
857+
}
858+
else if (context.IsFastClonerAvailable)
859+
{
860+
// Use runtime FastCloner for elements that require it
861+
itemExpr = $"({member.ElementTypeName})FastCloner.DeepClone(source[{indexList}])";
862+
}
863+
else
864+
{
865+
// Fallback to shallow copy
866+
itemExpr = $"source[{indexList}]";
867+
}
868+
869+
sb.AppendLine($"{innerIndent}result[{indexList}] = {itemExpr};");
870+
}
871+
872+
sb.AppendLine();
873+
sb.AppendLine(" return result;");
874+
sb.AppendLine(" }");
875+
sb.AppendLine();
876+
}
877+
878+
/// <summary>
879+
/// Creates the correct array instantiation expression for both regular and jagged arrays.
880+
/// For regular arrays like int[], creates: new int[size]
881+
/// For jagged arrays like int[][], creates: new int[size][] (not new int[][size] which is invalid)
882+
/// </summary>
883+
private static string GetArrayCreationExpression(string elementTypeName, string sizeExpression)
884+
{
885+
// Check if the element type is itself an array (jagged array scenario)
886+
// e.g., for int[][], elementTypeName is "int[]"
887+
var bracketIndex = elementTypeName.IndexOf('[');
888+
889+
if (bracketIndex >= 0)
890+
{
891+
// Jagged array: element type contains brackets
892+
// Split into base type and trailing brackets
893+
// int[] → baseType="int", trailingBrackets="[]"
894+
// int[][] → baseType="int", trailingBrackets="[][]"
895+
var baseType = elementTypeName.Substring(0, bracketIndex);
896+
var trailingBrackets = elementTypeName.Substring(bracketIndex);
897+
898+
// Create: new baseType[size]trailingBrackets
899+
// e.g., new int[source.Length][] for int[][] array
900+
return $"new {baseType}[{sizeExpression}]{trailingBrackets}";
901+
}
902+
else
903+
{
904+
// Regular array: element type has no brackets
905+
// Create: new elementTypeName[size]
906+
return $"new {elementTypeName}[{sizeExpression}]";
907+
}
908+
}
909+
757910
private static void WriteHelperMethodSignature(CloneGeneratorContext context, string typeName, string methodName, bool needsState, bool isValueType)
758911
{
759912
var typeParams = GetTypeParametersString(context.Model);

src/FastCloner.SourceGenerator/ImplicitTypeAnalyzer.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ bool TryHandleComponent(ITypeSymbol componentType, out MemberModel? componentMem
9696
RequiresFastCloner: false,
9797
CollectionKind: CollectionKind.None,
9898
ConcreteTypeFullName: null,
99-
IsValueType: componentType.IsValueType
99+
IsValueType: componentType.IsValueType,
100+
IsInitOnly: false,
101+
HasPrivateSetter: false,
102+
ArrayRank: 0
100103
);
101104
return true;
102105
}

0 commit comments

Comments
 (0)