Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start implementing IEquatable<Self> for all [SpacetimeDB.Type]s #2396

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 103 additions & 3 deletions crates/bindings-csharp/BSATN.Codegen/Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,18 @@ public BaseTypeDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter
public Scope.Extensions ToExtensions()
{
string read,
write;
write,
getHashCode,
toString;

var extensions = new Scope.Extensions(Scope, FullName);

var bsatnDecls = Members.Cast<MemberDeclaration>();
var bsatnDeclsWithoutTag = Members.Cast<MemberDeclaration>();
var bsatnDecls = bsatnDeclsWithoutTag;
var fieldNames = bsatnDecls.Select(m => m.Name);

extensions.BaseTypes.Add($"System.IEquatable<{ShortName}>");

if (Kind is TypeKind.Sum)
{
extensions.Contents.Append(
Expand Down Expand Up @@ -203,6 +208,35 @@ internal enum @enum: byte
)}}
}
""";

getHashCode = $$"""
switch (this) {
{{string.Join(
"\n",
bsatnDeclsWithoutTag.Select(decl => $"""
case {decl.Name}(var inner):
return inner.GetHashCode();
""")
)}}
default:
return 0;
}
""";

toString = $$"""
switch (this) {
{{string.Join(
"\n",
// escaped enough for you?
fieldNames.Select(name => $$$"""
case {{{name}}}(var inner):
return $"{{{name}}}({inner})";
""")
)}}
default:
return "UNKNOWN";
}
""";
}
else
{
Expand All @@ -224,11 +258,28 @@ public void WriteFields(System.IO.BinaryWriter writer) {
)}}
}
"""
);
);

read = $"SpacetimeDB.BSATN.IStructuralReadWrite.Read<{FullName}>(reader)";

write = "value.WriteFields(writer);";


getHashCode = $$"""
return {{string.Join(
" ^\n",
fieldNames.Select(name => $"{name}.GetHashCode()")
)}};
""";

// Warning:
// Looking at the following code too closely will drive you mad.
toString = $$"""
return $"{{ShortName}}({{string.Join(
", ",
fieldNames.Select(name => $$"""{{name}} = { {{name}} }""")
)}}";
""";
}

extensions.Contents.Append(
Expand All @@ -251,9 +302,58 @@ public SpacetimeDB.BSATN.AlgebraicType.Ref GetAlgebraicType(SpacetimeDB.BSATN.IT
SpacetimeDB.BSATN.AlgebraicType SpacetimeDB.BSATN.IReadWrite<{{FullName}}>.GetAlgebraicType(SpacetimeDB.BSATN.ITypeRegistrar registrar) =>
GetAlgebraicType(registrar);
}


public override int GetHashCode()
{
{{getHashCode}}
}

public override string ToString() {
{{toString}}
}
"""
);

if (!Scope.IsRecord)
{
// If we're not a record, override various equality things.

var equalsOverride = Scope.IsStruct ? "" : "override";

extensions.Contents.Append($$"""
public {{equalsOverride}} bool Equals({{FullName}} that)
{
return {{string.Join(
" &&\n",
fieldNames.Select(name => $"{name}.Equals(that.{name})")
)}};
}

public override bool Equals(object? that) {
if (that == null) {
return false;
}
var that_ = that as {{FullName}}?;
if (that_ == null) {
return false;
}
return Equals(that);
}

public static bool operator == ({{FullName}} this_, {{FullName}} that) {
if (((object)this_) == null || ((object)that) == null) {
return Object.Equals(this_, that);
}
return this_.Equals(that);
}

public static bool operator != ({{FullName}} this_, {{FullName}} that) {
return !(this_ == that);
}
""");
}

return extensions;
}
}
Expand Down
44 changes: 37 additions & 7 deletions crates/bindings-csharp/BSATN.Codegen/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,41 @@ IncrementalGeneratorInitializationContext context
public static string MakeRwTypeParam(string typeParam) => typeParam + "RW";

public class UnresolvedTypeException(INamedTypeSymbol type)
: InvalidOperationException($"Could not resolve type {type}") { }

public static string GetTypeInfo(ITypeSymbol type)
: InvalidOperationException($"Could not resolve type {type}")
{ }

/// <summary>
/// Return whether a type is a nullable, non-value type.
/// For example, `string?`.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static bool IsNullableNonValueType(ITypeSymbol type)
{
// We need to distinguish handle nullable reference types specially:
// compiler expands something like `int?` to `System.Nullable<int>` with the nullable annotation set to `Annotated`
// while something like `string?` is expanded to `string` with the nullable annotation set to `Annotated`...
// Beautiful design requires beautiful hacks.
if (
type.NullableAnnotation == NullableAnnotation.Annotated
&& type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T
)
return type.NullableAnnotation == NullableAnnotation.Annotated
&& type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T;
}

/// <summary>
/// Get the BSATN struct name for a type.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="UnresolvedTypeException"></exception>
public static string GetTypeInfo(ITypeSymbol type)
{
if (IsNullableNonValueType(type))
{
// If we're here, then this is a nullable reference type like `string?` and the original definition is `string`.
type = type.WithNullableAnnotation(NullableAnnotation.None);
return $"SpacetimeDB.BSATN.RefOption<{type}, {GetTypeInfo(type)}>";
}

return type switch
{
ITypeParameterSymbol typeParameter => MakeRwTypeParam(typeParameter.Name),
Expand Down Expand Up @@ -272,6 +290,18 @@ public Scope(MemberDeclarationSyntax? node)
namespaces = new(namespaces_.ToImmutable());
}

/// <returns>Whether this Scope is a struct declaration.</returns>
public bool IsStruct
{
get => typeScopes[0].Keyword == "struct";
}

/// <returns>Whether this Scope is a record declaration.</returns>
public bool IsRecord
{
get => typeScopes[0].Keyword == "record";
}

public readonly record struct TypeScope(string Keyword, string Name, string Constraints);

public sealed record Extensions(Scope Scope, string FullName)
Expand Down
1 change: 1 addition & 0 deletions crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<Version>1.0.0</Version>
<Title>SpacetimeDB BSATN Runtime</Title>
<Description>The SpacetimeDB BSATN Runtime implements APIs for BSATN serialization/deserialization in C#.</Description>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

<PropertyGroup>
Expand Down
29 changes: 26 additions & 3 deletions crates/bindings-csharp/BSATN.Runtime/BSATN/AlgebraicType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ public interface ITypeRegistrar
}

[SpacetimeDB.Type]
public partial struct AggregateElement(string? name, AlgebraicType algebraicType)
public partial struct AggregateElement
{
public string? Name = name;
public AlgebraicType AlgebraicType = algebraicType;
public string? Name;

public AlgebraicType AlgebraicType;

public AggregateElement(string name, AlgebraicType algebraicType)
{
Name = name;
AlgebraicType = algebraicType;
}
}

[SpacetimeDB.Type]
Expand Down Expand Up @@ -43,3 +50,19 @@ Unit F64
internal static AlgebraicType MakeOption(AlgebraicType someType) =>
new Sum([new("some", someType), new("none", Unit)]);
}

[SpacetimeDB.Type]
internal partial struct TestStruct
{
public int? NullableInt;
}

[SpacetimeDB.Type]
internal partial record TestEnum : SpacetimeDB.TaggedEnum<(int? ThingOne, string? ThingTwo)>;

[SpacetimeDB.Type]
internal partial record TestRecord
{
public int? NullableInt;
public string? NullableString;
}
Loading