switch root namespace name to NITLOAD

This commit is contained in:
Youmu Liu
2026-09-02 18:15:03 +08:00
parent 4218baad48
commit 7c7101ee48
502 changed files with 710 additions and 721 deletions
@@ -0,0 +1,429 @@
// -----------------------------------------------------------------------------
// file="GlFunctionFamilyAnalyzer"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using StgSharp.GenerateGL.Naming;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Analysis
{
/// <summary>
/// Builds inspectable function-family candidates without changing generation.
/// </summary>
internal static class GlFunctionFamilyAnalyzer
{
public static GlFunctionFamilyAnalysis Analyze(
GlRegistryModel registry
)
{
ArgumentNullException.ThrowIfNull(registry);
return Analyze(registry, registry.Commands);
}
public static GlFunctionFamilyAnalysis Analyze(
GlRegistryProjection projection
)
{
ArgumentNullException.ThrowIfNull(projection);
return Analyze(projection.Source, projection.Commands);
}
private static GlFunctionFamilyAnalysis Analyze(
GlRegistryModel registry,
IReadOnlyList<GlCommandDefinition> commands
)
{
Dictionary<string, IReadOnlyList<GlCommandDefinition>> commandsByName = commands
.GroupBy(value => value.Name, StringComparer.Ordinal)
.ToDictionary(
value => value.Key,
value => (IReadOnlyList<GlCommandDefinition>)Array.AsReadOnly(
value.ToArray()),
StringComparer.Ordinal);
Dictionary<string, GlFunctionNameShape> shapes =
GlFunctionNameFactorizer.Factor(registry, commands);
Dictionary<string, FamilyBuilder> builders = new Dictionary<string, FamilyBuilder>(
StringComparer.Ordinal);
List<GlFunctionRelation> relations = new List<GlFunctionRelation>();
foreach (GlCommandDefinition command in commands)
{
if (shapes.TryGetValue(command.Name, out GlFunctionNameShape? shape))
{
ReadBuilder(builders, shape.FamilyName).Add(command, shape);
}
}
AddRegistryRelations(commands, commandsByName, shapes, builders, relations);
AddSingleValuePointerRelations(commandsByName, shapes, builders, relations);
GlFunctionFamily[] families = builders.Values
.Where(value => value.Members.Count > 1 || value.Relations.Count != 0)
.Select(value => value.Build())
.OrderBy(value => value.Name, StringComparer.Ordinal)
.ToArray();
HashSet<string> classifiedNames = families
.SelectMany(value => value.Members)
.Select(value => value.Command.Name)
.ToHashSet(StringComparer.Ordinal);
GlCommandDefinition[] unclassified = commands
.Where(value => !classifiedNames.Contains(value.Name))
.OrderBy(value => value.Name, StringComparer.Ordinal)
.ToArray();
return new GlFunctionFamilyAnalysis(
Freeze(families),
Freeze(unclassified),
Freeze(relations));
}
private static void AddRegistryRelations(
IReadOnlyList<GlCommandDefinition> commands,
Dictionary<string, IReadOnlyList<GlCommandDefinition>> commandsByName,
Dictionary<string, GlFunctionNameShape> shapes,
IDictionary<string, FamilyBuilder> builders,
List<GlFunctionRelation> relations
)
{
foreach (GlCommandDefinition command in commands)
{
foreach (GlNamedReference reference in command.VectorEquivalents)
{
bool exists = commandsByName.TryGetValue(
reference.Name,
out IReadOnlyList<GlCommandDefinition>? targets);
GlFunctionRelation relation = new GlFunctionRelation(
command.Name,
reference.Name,
GlFunctionRelationKind.RegistryVectorEquivalent,
exists
? GlFunctionRelationState.Verified
: GlFunctionRelationState.MissingTarget,
exists
? "declared by gl.xml <vecequiv>"
: "gl.xml <vecequiv> target is not registered");
relations.Add(relation);
string familyName = ReadRelationFamilyName(
command.Name,
reference.Name,
shapes);
FamilyBuilder builder = ReadBuilder(builders, familyName);
builder.Add(
command,
shapes.TryGetValue(command.Name, out GlFunctionNameShape? sourceShape)
? sourceShape
: null);
builder.Relations.Add(relation);
if (!exists)
{
builder.Diagnostics.Add(
$"Missing vector-equivalent target '{reference.Name}'.");
continue;
}
foreach (GlCommandDefinition target in targets!)
{
builder.Add(
target,
shapes.TryGetValue(target.Name, out GlFunctionNameShape? targetShape)
? targetShape
: null);
}
}
}
}
private static void AddSingleValuePointerRelations(
Dictionary<string, IReadOnlyList<GlCommandDefinition>> commandsByName,
Dictionary<string, GlFunctionNameShape> shapes,
IDictionary<string, FamilyBuilder> builders,
List<GlFunctionRelation> relations
)
{
HashSet<string> existingPairs = relations
.Select(value => $"{value.SourceName}\0{value.TargetName}")
.ToHashSet(StringComparer.Ordinal);
foreach ((string commandName, GlFunctionNameShape pointerShape) in shapes)
{
if (!pointerShape.IsVectorForm ||
!commandsByName.TryGetValue(
commandName,
out IReadOnlyList<GlCommandDefinition>? pointerCommands))
{
continue;
}
string scalarName = commandName[..^1];
if (!shapes.TryGetValue(scalarName, out GlFunctionNameShape? scalarShape) ||
!commandsByName.TryGetValue(
scalarName,
out IReadOnlyList<GlCommandDefinition>? scalarCommands))
{
continue;
}
foreach (GlCommandDefinition pointer in pointerCommands)
{
if (pointer.Parameters.Count == 0 ||
!string.Equals(pointer.Parameters[^1].Length, "1", StringComparison.Ordinal))
{
continue;
}
foreach (GlCommandDefinition scalar in scalarCommands)
{
if (existingPairs.Contains($"{scalar.Name}\0{pointer.Name}"))
{
continue;
}
bool valid = IsSingleValuePointerPair(scalar, pointer, out string detail);
GlFunctionRelation relation = new GlFunctionRelation(
scalar.Name,
pointer.Name,
GlFunctionRelationKind.InferredSingleValuePointer,
valid
? GlFunctionRelationState.Verified
: GlFunctionRelationState.Rejected,
detail);
relations.Add(relation);
FamilyBuilder builder = ReadBuilder(builders, pointerShape.FamilyName);
builder.Add(scalar, scalarShape);
builder.Add(pointer, pointerShape);
builder.Relations.Add(relation);
if (!valid)
{
builder.Diagnostics.Add(
$"Rejected scalar/pointer pair {scalar.Name} -> {pointer.Name}: " +
detail);
}
}
}
}
}
private static bool IsSingleValuePointerPair(
GlCommandDefinition scalar,
GlCommandDefinition pointer,
out string detail
)
{
if (!string.Equals(
scalar.Return.TypeName,
pointer.Return.TypeName,
StringComparison.Ordinal) ||
scalar.Parameters.Count != pointer.Parameters.Count)
{
detail = "return type or parameter count differs";
return false;
}
int lastIndex = scalar.Parameters.Count - 1;
for (int index = 0; index < lastIndex; index++)
{
if (!HaveEquivalentParameterTypes(
scalar.Parameters[index],
pointer.Parameters[index]))
{
detail = $"parameter {index + 1} differs";
return false;
}
}
GlParameterDefinition scalarValue = scalar.Parameters[lastIndex];
GlParameterDefinition pointerValue = pointer.Parameters[lastIndex];
if (!string.Equals(
scalarValue.TypeName,
pointerValue.TypeName,
StringComparison.Ordinal) ||
scalarValue.DeclarationText.Contains('*', StringComparison.Ordinal) ||
!pointerValue.DeclarationText.Contains('*', StringComparison.Ordinal) ||
!pointerValue.DeclarationText.TrimStart().StartsWith(
"const ",
StringComparison.Ordinal) ||
!string.Equals(pointerValue.Length, "1", StringComparison.Ordinal))
{
detail = "last parameter is not a matching T / const T* len=1 pair";
return false;
}
detail = "verified T / const T* len=1 pair";
return true;
}
private static bool HaveEquivalentParameterTypes(
GlParameterDefinition left,
GlParameterDefinition right
)
{
return string.Equals(left.TypeName, right.TypeName, StringComparison.Ordinal) &&
left.DeclarationText.Contains('*', StringComparison.Ordinal) ==
right.DeclarationText.Contains('*', StringComparison.Ordinal);
}
private static string ReadRelationFamilyName(
string sourceName,
string targetName,
Dictionary<string, GlFunctionNameShape> shapes
)
{
if (shapes.TryGetValue(sourceName, out GlFunctionNameShape? sourceShape))
{
return sourceShape.FamilyName;
}
if (shapes.TryGetValue(targetName, out GlFunctionNameShape? targetShape))
{
return targetShape.FamilyName;
}
string source = RemoveGlPrefix(sourceName);
string target = RemoveGlPrefix(targetName);
if (target.EndsWith('v') &&
string.Equals(source, target[..^1], StringComparison.Ordinal))
{
return CSharpIdentifierPolicy.NormalizeMemberName(source);
}
int length = 0;
int maximum = Math.Min(source.Length, target.Length);
while (length < maximum && source[length] == target[length])
{
length++;
}
string familyName = length == 0 ? source : source[..length].TrimEnd('_');
return CSharpIdentifierPolicy.NormalizeMemberName(familyName);
}
private static string RemoveGlPrefix(
string name
)
{
return name.StartsWith("gl", StringComparison.Ordinal) && name.Length > 2
? name[2..]
: name;
}
private static FamilyBuilder ReadBuilder(
IDictionary<string, FamilyBuilder> builders,
string familyName
)
{
if (!builders.TryGetValue(familyName, out FamilyBuilder? builder))
{
builder = new FamilyBuilder(familyName);
builders.Add(familyName, builder);
}
return builder;
}
private static ReadOnlyCollection<T> Freeze<T>(
IEnumerable<T> values
)
{
return Array.AsReadOnly(values.ToArray());
}
private sealed class FamilyBuilder
{
private readonly Dictionary<string, GlFunctionFamilyMember> _members = new Dictionary<string, GlFunctionFamilyMember>(
StringComparer.Ordinal);
public FamilyBuilder(
string name
)
{
Name = name;
}
public string Name { get; }
public Dictionary<string, GlFunctionFamilyMember>.ValueCollection Members =>
_members.Values;
public List<GlFunctionRelation> Relations { get; } = new List<GlFunctionRelation>();
public List<string> Diagnostics { get; } = new List<string>();
public void Add(
GlCommandDefinition command,
GlFunctionNameShape? shape
)
{
_members.TryAdd(
command.Name,
new GlFunctionFamilyMember(command, shape));
}
public GlFunctionFamily Build()
{
GlFunctionFamilyMember[] members = _members.Values
.OrderBy(value => value.Command.Name, StringComparer.Ordinal)
.ToArray();
string[] diagnostics = Diagnostics
.Distinct(StringComparer.Ordinal)
.OrderBy(value => value, StringComparer.Ordinal)
.ToArray();
return new GlFunctionFamily(
Name,
Freeze(members),
Freeze(Relations.Distinct().ToArray()),
Freeze(diagnostics),
ReadShapeNamePolicy(members));
}
private static GlFunctionShapeNamePolicy ReadShapeNamePolicy(
IReadOnlyCollection<GlFunctionFamilyMember> members
)
{
GlFunctionFamilyMember[] scalarMembers = members
.Where(
value => value.NameShape is
{ IsVectorForm: false, Shape: not null })
.ToArray();
if (scalarMembers.Select(value => value.NameShape!.Shape)
.Distinct(StringComparer.Ordinal)
.Count() < 2)
{
return GlFunctionShapeNamePolicy.NotApplicable;
}
bool collision = scalarMembers
.GroupBy(ReadNativeSignature, StringComparer.Ordinal)
.Any(group => group.Select(value => value.NameShape!.Shape)
.Distinct(StringComparer.Ordinal)
.Count() > 1);
return collision
? GlFunctionShapeNamePolicy.MustRetain
: GlFunctionShapeNamePolicy.CanRemove;
}
private static string ReadNativeSignature(
GlFunctionFamilyMember member
)
{
GlCommandDefinition command = member.Command;
return $"{command.Return.TypeName}:" + string.Join(
",",
command.Parameters.Select(
value => $"{value.TypeName}:{value.DeclarationText.Contains('*', StringComparison.Ordinal)}"));
}
}
}
}
@@ -0,0 +1,68 @@
// -----------------------------------------------------------------------------
// file="GlFunctionFamilyModel"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.Collections.Generic;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Analysis
{
internal sealed record GlFunctionNameShape(
string FamilyName,
string? Shape,
int? Columns,
int? Rows,
string TypeSuffix,
bool IsVectorForm
);
internal sealed record GlFunctionFamilyMember(
GlCommandDefinition Command,
GlFunctionNameShape? NameShape
);
internal sealed record GlFunctionRelation(
string SourceName,
string TargetName,
GlFunctionRelationKind Kind,
GlFunctionRelationState State,
string Detail
);
internal sealed record GlFunctionFamily(
string Name,
IReadOnlyList<GlFunctionFamilyMember> Members,
IReadOnlyList<GlFunctionRelation> Relations,
IReadOnlyList<string> Diagnostics,
GlFunctionShapeNamePolicy ShapeNamePolicy
);
internal sealed record GlFunctionFamilyAnalysis(
IReadOnlyList<GlFunctionFamily> Families,
IReadOnlyList<GlCommandDefinition> UnclassifiedCommands,
IReadOnlyList<GlFunctionRelation> Relations
);
internal enum GlFunctionRelationKind
{
RegistryVectorEquivalent,
InferredSingleValuePointer,
}
internal enum GlFunctionRelationState
{
Verified,
MissingTarget,
Rejected,
}
internal enum GlFunctionShapeNamePolicy
{
NotApplicable,
CanRemove,
MustRetain,
}
}
@@ -0,0 +1,472 @@
// -----------------------------------------------------------------------------
// file="GlFunctionNameFactorizer"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using StgSharp.GenerateGL.Naming;
using StgSharp.GenerateGL.Registry;
namespace StgSharp.GenerateGL.Analysis
{
/// <summary>
/// Factors command-name alternatives by their common prefix and interprets only the
/// remaining suffix as overload axes.
/// </summary>
internal static class GlFunctionNameFactorizer
{
private static readonly string[] _typeSuffixes =
{
"ui64",
"i64",
"ui",
"ub",
"us",
"b",
"s",
"i",
"f",
"d",
"h",
"x",
};
public static Dictionary<string, GlFunctionNameShape> Factor(
GlRegistryModel registry,
IReadOnlyList<GlCommandDefinition> commands
)
{
ArgumentNullException.ThrowIfNull(registry);
ArgumentNullException.ThrowIfNull(commands);
string[] vendorSuffixes = ReadVendorSuffixes(registry);
NameEntry[] entries = commands
.Select(
command => new NameEntry(
command,
command.Name,
ReadCoreName(command.Name, vendorSuffixes)))
.ToArray();
TrieNode root = new TrieNode(string.Empty);
for (int index = 0; index < entries.Length; index++)
{
Add(root, entries[index].CoreName, index);
}
List<FactorCandidate>[] candidatesByEntry = Enumerable.Range(0, entries.Length)
.Select(_ => new List<FactorCandidate>())
.ToArray();
CollectCandidates(root, entries, candidatesByEntry);
Dictionary<string, GlFunctionNameShape> result = new Dictionary<string, GlFunctionNameShape>(
StringComparer.Ordinal);
for (int index = 0; index < entries.Length; index++)
{
FactorCandidate? candidate = candidatesByEntry[index]
.OrderByDescending(value => value.DistinctVariantCount)
.ThenByDescending(value => value.MemberCount)
.ThenBy(value => value.FamilyName.Length)
.FirstOrDefault();
if (candidate is null ||
!candidate.Shapes.TryGetValue(index, out GlFunctionNameShape? shape))
{
continue;
}
result.TryAdd(entries[index].CommandName, shape);
}
AddFixedVectorPairs(result, commands);
return result;
}
private static void AddFixedVectorPairs(
Dictionary<string, GlFunctionNameShape> shapes,
IReadOnlyList<GlCommandDefinition> commands
)
{
Dictionary<string, GlCommandDefinition> commandsByName = commands.ToDictionary(
value => value.Name,
StringComparer.Ordinal);
foreach (GlCommandDefinition vector in commands)
{
if (!vector.Name.EndsWith('v') ||
shapes.ContainsKey(vector.Name) ||
vector.Parameters.Count == 0 ||
!int.TryParse(vector.Parameters[^1].Length, out int componentCount) ||
componentCount < 2)
{
continue;
}
string scalarName = vector.Name[..^1];
if (shapes.ContainsKey(scalarName) ||
!commandsByName.TryGetValue(
scalarName,
out GlCommandDefinition? scalar) ||
!IsFixedVectorPair(scalar, vector, componentCount))
{
continue;
}
string familyName = CSharpIdentifierPolicy.NormalizeMemberName(
scalarName.StartsWith("gl", StringComparison.Ordinal) &&
scalarName.Length > 2
? scalarName[2..]
: scalarName);
shapes.Add(
scalarName,
new GlFunctionNameShape(
familyName,
null,
null,
null,
string.Empty,
false));
shapes.Add(
vector.Name,
new GlFunctionNameShape(
familyName,
null,
null,
null,
string.Empty,
true));
}
}
private static bool IsFixedVectorPair(
GlCommandDefinition scalar,
GlCommandDefinition vector,
int componentCount
)
{
if (!string.Equals(
scalar.Return.TypeName,
vector.Return.TypeName,
StringComparison.Ordinal) ||
scalar.Parameters.Count != vector.Parameters.Count - 1 + componentCount)
{
return false;
}
int prefixCount = vector.Parameters.Count - 1;
for (int index = 0; index < prefixCount; index++)
{
if (!HaveEquivalentScalarTypes(
scalar.Parameters[index].TypeName,
vector.Parameters[index].TypeName) ||
scalar.Parameters[index].DeclarationText.Contains('*', StringComparison.Ordinal) !=
vector.Parameters[index].DeclarationText.Contains('*', StringComparison.Ordinal))
{
return false;
}
}
GlParameterDefinition vectorValue = vector.Parameters[^1];
if (!vectorValue.DeclarationText.Contains('*', StringComparison.Ordinal))
{
return false;
}
for (int index = prefixCount; index < scalar.Parameters.Count; index++)
{
if (!HaveEquivalentScalarTypes(
scalar.Parameters[index].TypeName,
vectorValue.TypeName) ||
scalar.Parameters[index].DeclarationText.Contains('*', StringComparison.Ordinal))
{
return false;
}
}
return true;
}
private static bool HaveEquivalentScalarTypes(
string? left,
string? right
)
{
if (string.Equals(left, right, StringComparison.Ordinal))
{
return true;
}
return left is "GLint" or "GLsizei" &&
right is "GLint" or "GLsizei";
}
private static List<int> CollectCandidates(
TrieNode node,
IReadOnlyList<NameEntry> entries,
IReadOnlyList<List<FactorCandidate>> candidatesByEntry
)
{
List<int> descendants = new List<int>(node.Terminals);
foreach (TrieNode child in node.Children.Values)
{
descendants.AddRange(CollectCandidates(child, entries, candidatesByEntry));
}
if (node.Prefix.Length == 0 || descendants.Count < 2)
{
return descendants;
}
Dictionary<int, GlFunctionNameShape> shapes = new Dictionary<int, GlFunctionNameShape>();
foreach (int index in descendants)
{
string coreName = entries[index].CoreName;
if (coreName.Length <= node.Prefix.Length ||
!TryParseVariantSuffix(
coreName[node.Prefix.Length..],
node.Prefix,
entries[index].Command,
out GlFunctionNameShape? shape))
{
continue;
}
shapes.Add(index, shape!);
}
int distinctVariantCount = shapes.Keys
.Select(index => entries[index].CoreName)
.Distinct(StringComparer.Ordinal)
.Count();
if (distinctVariantCount < 2)
{
return descendants;
}
FactorCandidate candidate = new FactorCandidate(
node.Prefix,
shapes,
distinctVariantCount);
foreach (int index in shapes.Keys)
{
candidatesByEntry[index].Add(candidate);
}
return descendants;
}
private static bool TryParseVariantSuffix(
string suffix,
string familyName,
GlCommandDefinition command,
out GlFunctionNameShape? shape
)
{
bool isVectorForm = suffix.EndsWith('v');
if (isVectorForm)
{
suffix = suffix[..^1];
}
string? typeSuffix = null;
foreach (string candidate in _typeSuffixes)
{
if (suffix.EndsWith(candidate, StringComparison.Ordinal))
{
typeSuffix = candidate;
suffix = suffix[..^candidate.Length];
break;
}
}
if (typeSuffix is null ||
!IsTypeSuffixConsistent(command, typeSuffix) ||
!TryParseShape(
suffix,
out string? shapeText,
out int? columns,
out int? rows))
{
shape = null;
return false;
}
shape = new GlFunctionNameShape(
CSharpIdentifierPolicy.NormalizeMemberName(familyName),
shapeText,
columns,
rows,
typeSuffix,
isVectorForm);
return true;
}
private static bool IsTypeSuffixConsistent(
GlCommandDefinition command,
string typeSuffix
)
{
string? glType = typeSuffix switch
{
"b" => "GLbyte",
"ub" => "GLubyte",
"s" => "GLshort",
"us" => "GLushort",
"i" => "GLint",
"ui" => "GLuint",
"i64" => "GLint64",
"ui64" => "GLuint64",
"f" => "GLfloat",
"d" => "GLdouble",
"h" => "GLhalf",
"x" => "GLfixed",
_ => null,
};
if (glType is null)
{
return false;
}
return string.Equals(command.Return.TypeName, glType, StringComparison.Ordinal) ||
command.Parameters.Any(
value => string.Equals(
value.TypeName,
glType,
StringComparison.Ordinal));
}
private static bool TryParseShape(
string value,
out string? shape,
out int? columns,
out int? rows
)
{
if (value.Length == 0)
{
shape = null;
columns = null;
rows = null;
return true;
}
if (value.Length == 1 && value[0] is >= '1' and <= '4')
{
shape = value;
columns = value[0] - '0';
rows = 1;
return true;
}
if (value.Length == 3 &&
value[0] is >= '2' and <= '4' &&
value[1] == 'x' &&
value[2] is >= '2' and <= '4')
{
shape = value;
columns = value[0] - '0';
rows = value[2] - '0';
return true;
}
shape = null;
columns = null;
rows = null;
return false;
}
private static void Add(
TrieNode root,
string value,
int entryIndex
)
{
TrieNode current = root;
foreach (char character in value)
{
if (!current.Children.TryGetValue(character, out TrieNode? child))
{
child = new TrieNode(current.Prefix + character);
current.Children.Add(character, child);
}
current = child;
}
current.Terminals.Add(entryIndex);
}
private static string ReadCoreName(
string commandName,
IReadOnlyList<string> vendorSuffixes
)
{
string name = commandName.StartsWith("gl", StringComparison.Ordinal) &&
commandName.Length > 2
? commandName[2..]
: commandName;
foreach (string vendorSuffix in vendorSuffixes)
{
if (name.Length > vendorSuffix.Length && name.EndsWith(
vendorSuffix,
StringComparison.Ordinal))
{
return name[..^vendorSuffix.Length];
}
}
return name;
}
private static string[] ReadVendorSuffixes(
GlRegistryModel registry
)
{
return registry.Extensions
.Select(value => value.Name.Split('_', StringSplitOptions.RemoveEmptyEntries))
.Where(value => value.Length > 2 && value[0] == "GL")
.Select(value => value[1])
.Where(value => value.All(character => char.IsUpper(character) || char.IsDigit(character)))
.Distinct(StringComparer.Ordinal)
.OrderByDescending(value => value.Length)
.ThenBy(value => value, StringComparer.Ordinal)
.ToArray();
}
private sealed record NameEntry(
GlCommandDefinition Command,
string CommandName,
string CoreName
);
private sealed record FactorCandidate(
string FamilyName,
IReadOnlyDictionary<int, GlFunctionNameShape> Shapes,
int DistinctVariantCount
)
{
public int MemberCount => Shapes.Count;
}
private sealed class TrieNode
{
public TrieNode(
string prefix
)
{
Prefix = prefix;
}
public string Prefix { get; }
public Dictionary<char, TrieNode> Children { get; } = new Dictionary<char, TrieNode>();
public List<int> Terminals { get; } = new List<int>();
}
}
}