From b41d29416fe499c3f540b0d4a54a64a9ebd21d31 Mon Sep 17 00:00:00 2001 From: Stable Genius <259448942+stablegenius49@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:15:17 -0800 Subject: [PATCH 1/2] fix(csharp-codegen): escape C# reserved keywords in generated identifiers - Add Identifier property to MemberDeclaration that prefixes C# keywords with @ - Use SyntaxFacts.GetKeywordKind to detect reserved keywords - Update all code generation sites to use Identifier instead of Name - Add test for keyword field names (class, params) in tables/reducers/procedures Fixes #4529 --- crates/bindings-csharp/BSATN.Codegen/Type.cs | 35 +++++---- crates/bindings-csharp/Codegen.Tests/Tests.cs | 54 ++++++++++++++ crates/bindings-csharp/Codegen/Module.cs | 73 +++++++++++-------- 3 files changed, 117 insertions(+), 45 deletions(-) diff --git a/crates/bindings-csharp/BSATN.Codegen/Type.cs b/crates/bindings-csharp/BSATN.Codegen/Type.cs index d9d51fcaaa6..73e9be2f950 100644 --- a/crates/bindings-csharp/BSATN.Codegen/Type.cs +++ b/crates/bindings-csharp/BSATN.Codegen/Type.cs @@ -422,6 +422,15 @@ public MemberDeclaration(ISymbol member, ITypeSymbol type, DiagReporter diag) public MemberDeclaration(IFieldSymbol field, DiagReporter diag) : this(field, field.Type, diag) { } + public string Identifier => EscapeIdentifier(Name); + + private static string EscapeIdentifier(string name) + { + var kind = SyntaxFacts.GetKeywordKind(name); + var contextualKind = SyntaxFacts.GetContextualKeywordKind(name); + return kind != SyntaxKind.None || contextualKind != SyntaxKind.None ? $"@{name}" : name; + } + public static string GenerateBsatnFields( Accessibility visibility, IEnumerable members @@ -431,7 +440,7 @@ IEnumerable members return string.Join( "\n ", members.Select(m => - $"{visStr} static readonly {m.Type.ToBSATNString()} {m.Name}{TypeUse.BsatnFieldSuffix} = new();" + $"{visStr} static readonly {m.Type.ToBSATNString()} {m.Identifier}{TypeUse.BsatnFieldSuffix} = new();" ) ); } @@ -442,7 +451,7 @@ public static string GenerateDefs(IEnumerable members) => // we can't use nameof(m.Type.BsatnFieldName) because the bsatn field name differs from the logical name // assigned in the type. members.Select(m => - $"new(\"{m.Name}\", {m.Name}{TypeUse.BsatnFieldSuffix}.GetAlgebraicType(registrar))" + $"new(\"{m.Name}\", {m.Identifier}{TypeUse.BsatnFieldSuffix}.GetAlgebraicType(registrar))" ) ); } @@ -569,10 +578,10 @@ public Scope.Extensions ToExtensions() // To avoid this, we append an underscore to the field name. // In most cases the field name shouldn't matter anyway as you'll idiomatically use pattern matching to extract the value. $$""" - public sealed record {{m.Name}}({{m.Type.Name}} {{m.Name}}_) : {{ShortName}} + public sealed record {{m.Identifier}}({{m.Type.Name}} {{m.Identifier}}_) : {{ShortName}} { public override string ToString() => - $"{{m.Name}}({ SpacetimeDB.BSATN.StringUtil.GenericToString({{m.Name}}_) })"; + $"{{m.Name}}({ SpacetimeDB.BSATN.StringUtil.GenericToString({{m.Identifier}}_) })"; } """ @@ -585,7 +594,7 @@ public override string ToString() => {{string.Join( "\n ", bsatnDecls.Select((m, i) => - $"{i} => new {m.Name}({m.Name}{TypeUse.BsatnFieldSuffix}.Read(reader))," + $"{i} => new {m.Identifier}({m.Identifier}{TypeUse.BsatnFieldSuffix}.Read(reader))," ) )}} _ => throw new System.InvalidOperationException("Invalid tag value, this state should be unreachable.") @@ -597,9 +606,9 @@ public override string ToString() => {{string.Join( "\n", bsatnDecls.Select((m, i) => $""" - case {m.Name}(var inner): + case {m.Identifier}(var inner): writer.Write((byte){i}); - {m.Name}{TypeUse.BsatnFieldSuffix}.Write(writer, inner); + {m.Identifier}{TypeUse.BsatnFieldSuffix}.Write(writer, inner); break; """))}} } @@ -615,7 +624,7 @@ public override string ToString() => var hashName = $"___hash{member.Name}"; return $""" - case {member.Name}(var inner): + case {member.Identifier}(var inner): {member.Type.GetHashCodeStatement("inner", hashName)} return {hashName}; """; @@ -634,14 +643,14 @@ public override string ToString() => public void ReadFields(System.IO.BinaryReader reader) { {{string.Join( "\n", - bsatnDecls.Select(m => $" {m.Name} = BSATN.{m.Name}{TypeUse.BsatnFieldSuffix}.Read(reader);") + bsatnDecls.Select(m => $" {m.Identifier} = BSATN.{m.Identifier}{TypeUse.BsatnFieldSuffix}.Read(reader);") )}} } public void WriteFields(System.IO.BinaryWriter writer) { {{string.Join( "\n", - bsatnDecls.Select(m => $" BSATN.{m.Name}{TypeUse.BsatnFieldSuffix}.Write(writer, {m.Name});") + bsatnDecls.Select(m => $" BSATN.{m.Identifier}{TypeUse.BsatnFieldSuffix}.Write(writer, {m.Identifier});") )}} } @@ -661,7 +670,7 @@ object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() { public override string ToString() => $"{{ShortName}} {{start}} {{string.Join( ", ", - bsatnDecls.Select(m => $$"""{{m.Name}} = {SpacetimeDB.BSATN.StringUtil.GenericToString({{m.Name}})}""") + bsatnDecls.Select(m => $$"""{{m.Name}} = {SpacetimeDB.BSATN.StringUtil.GenericToString({{m.Identifier}})}""") )}} {{end}}"; """ ); @@ -680,7 +689,7 @@ public override string ToString() => var declHashName = (MemberDeclaration decl) => $"___hash{decl.Name}"; getHashCode = $$""" - {{string.Join("\n", bsatnDecls.Select(decl => decl.Type.GetHashCodeStatement(decl.Name, declHashName(decl))))}} + {{string.Join("\n", bsatnDecls.Select(decl => decl.Type.GetHashCodeStatement(decl.Identifier, declHashName(decl))))}} return {{JoinOrValue( " ^\n ", bsatnDecls.Select(declHashName), @@ -735,7 +744,7 @@ public override int GetHashCode() public bool Equals({{fullNameMaybeRef}} that) { {{(Scope.IsStruct ? "" : "if (((object?)that) == null) { return false; }\n ")}} - {{string.Join("\n", bsatnDecls.Select(decl => decl.Type.EqualsStatement($"this.{decl.Name}", $"that.{decl.Name}", declEqualsName(decl))))}} + {{string.Join("\n", bsatnDecls.Select(decl => decl.Type.EqualsStatement($"this.{decl.Identifier}", $"that.{decl.Identifier}", declEqualsName(decl))))}} return {{JoinOrValue( " &&\n ", bsatnDecls.Select(declEqualsName), diff --git a/crates/bindings-csharp/Codegen.Tests/Tests.cs b/crates/bindings-csharp/Codegen.Tests/Tests.cs index 8b617a161b2..1ad8c3eb463 100644 --- a/crates/bindings-csharp/Codegen.Tests/Tests.cs +++ b/crates/bindings-csharp/Codegen.Tests/Tests.cs @@ -256,6 +256,60 @@ public static async Task SettingsAndExplicitNames() AssertGeneratedCodeDoesNotUseInternalBound(compilationAfterGen); } + [Fact] + public static async Task CSharpKeywordIdentifiersAreEscapedInGeneratedCode() + { + var fixture = await Fixture.Compile("server"); + + const string source = + """ + using SpacetimeDB; + + [SpacetimeDB.Table] + public partial struct KeywordTable + { + [SpacetimeDB.PrimaryKey] + public ulong @class; + + public int @params; + } + + public static partial class KeywordApis + { + [SpacetimeDB.Reducer] + public static void KeywordReducer(ReducerContext ctx, int @params, string @class) + { + _ = @params; + _ = @class; + } + + [SpacetimeDB.Procedure] + public static int KeywordProcedure(ProcedureContext ctx, int @params, int @class) + { + return @params + @class; + } + } + """; + + var parseOptions = new CSharpParseOptions(fixture.SampleCompilation.LanguageVersion); + var tree = CSharpSyntaxTree.ParseText(source, parseOptions, path: "KeywordNames.cs"); + var compilation = fixture.SampleCompilation.AddSyntaxTrees(tree); + + var driver = CSharpGeneratorDriver.Create( + [new SpacetimeDB.Codegen.Type().AsSourceGenerator(), new SpacetimeDB.Codegen.Module().AsSourceGenerator()], + driverOptions: new( + disabledOutputs: IncrementalGeneratorOutputKind.None, + trackIncrementalGeneratorSteps: true + ), + parseOptions: parseOptions + ); + + var runResult = driver.RunGenerators(compilation).GetRunResult(); + var compilationAfterGen = compilation.AddSyntaxTrees(runResult.GeneratedTrees); + + Assert.Empty(GetCompilationErrors(compilationAfterGen)); + } + [Fact] public static async Task TestDiagnostics() { diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index e9c0d0522e5..1fec8a059ec 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -262,7 +262,7 @@ public ColumnAttrs GetAttrs(TableAccessor tableAccessor) => // For the `TableDesc` constructor. public string GenerateColumnDef() => - $"new (nameof({Name}), BSATN.{Name}{TypeUse.BsatnFieldSuffix}.GetAlgebraicType(registrar))"; + $"new (nameof({Identifier}), BSATN.{Identifier}{TypeUse.BsatnFieldSuffix}.GetAlgebraicType(registrar))"; } record Scheduled(string ReducerName, int ScheduledAtColumn); @@ -574,15 +574,15 @@ public IEnumerable GenerateTableAccessorFilters(TableAccessor tableAcces ? $"public {globalName} Update({globalName} row) => DoUpdate(row);" : ""; yield return $$""" - {{vis}} sealed class {{f.Name}}UniqueIndex : {{uniqueIndexBase}}<{{tableAccessor.Name}}, {{globalName}}, {{f.Type.Name}}, {{f.Type.BSATNName}}> { - internal {{f.Name}}UniqueIndex() : base("{{standardIndexName}}") {} + {{vis}} sealed class {{f.Identifier}}UniqueIndex : {{uniqueIndexBase}}<{{tableAccessor.Name}}, {{globalName}}, {{f.Type.Name}}, {{f.Type.BSATNName}}> { + internal {{f.Identifier}}UniqueIndex() : base("{{standardIndexName}}") {} // Important: don't move this to the base class. // C# generics don't play well with nullable types and can't accept both struct-type-based and class-type-based // `globalName` in one generic definition, leading to buggy `Row?` expansion for either one or another. public {{globalName}}? Find({{f.Type.Name}} key) => FindSingle(key); {{updateMethod}} } - {{vis}} {{f.Name}}UniqueIndex {{f.Name}} => new(); + {{vis}} {{f.Identifier}}UniqueIndex {{f.Identifier}} => new(); """; } @@ -610,10 +610,10 @@ public IEnumerable GenerateTableAccessorFilters(TableAccessor tableAcces ", ", members.Take(n + 1).Select(m => $"{m.Type.Name}, {m.Type.BSATNName}") ); - var scalars = members.Take(n).Select(m => $"{m.Type.Name} {m.Name}"); - var lastScalar = $"{members[n].Type.Name} {members[n].Name}"; + var scalars = members.Take(n).Select(m => $"{m.Type.Name} {m.Identifier}"); + var lastScalar = $"{members[n].Type.Name} {members[n].Identifier}"; var lastBounds = - $"global::SpacetimeDB.Bound<{members[n].Type.Name}> {members[n].Name}"; + $"global::SpacetimeDB.Bound<{members[n].Type.Name}> {members[n].Identifier}"; var argsScalar = string.Join(", ", scalars.Append(lastScalar)); var argsBounds = string.Join(", ", scalars.Append(lastBounds)); string argName; @@ -625,7 +625,7 @@ public IEnumerable GenerateTableAccessorFilters(TableAccessor tableAcces } else { - argName = members[0].Name; + argName = members[0].Identifier; } yield return $$""" @@ -668,19 +668,19 @@ private IEnumerable GenerateReadOnlyAccessorFilters(TableAccessor tableA var standardIndexName = ct.ToIndex().StandardIndexName(tableAccessor); yield return $$$""" - public sealed class {{{f.Name}}}Index + public sealed class {{{f.Identifier}}}Index : {{{uniqueIndexBase}}}< global::SpacetimeDB.Internal.ViewHandles.{{{tableAccessor.Name}}}ReadOnly, {{{globalName}}}, {{{f.Type.Name}}}, {{{f.Type.BSATNName}}}> { - internal {{{f.Name}}}Index() : base("{{{standardIndexName}}}") { } + internal {{{f.Identifier}}}Index() : base("{{{standardIndexName}}}") { } public {{{globalName}}}? Find({{{f.Type.Name}}} key) => FindSingle(key); } - public {{{f.Name}}}Index {{{f.Name}}} => new(); + public {{{f.Identifier}}}Index {{{f.Identifier}}} => new(); """; } @@ -714,19 +714,19 @@ public sealed class {{{name}}}Index ); var scalarArgs = string.Join( ", ", - declaringMembers.Select(m => $"{m.Type.Name} {m.Name}") + declaringMembers.Select(m => $"{m.Type.Name} {m.Identifier}") ); var boundsArgs = string.Join( ", ", declaringMembers .Take(n) - .Select(m => $"{m.Type.Name} {m.Name}") + .Select(m => $"{m.Type.Name} {m.Identifier}") .Append( - $"global::SpacetimeDB.Bound<{declaringMembers[^1].Type.Name}> {declaringMembers[^1].Name}" + $"global::SpacetimeDB.Bound<{declaringMembers[^1].Type.Name}> {declaringMembers[^1].Identifier}" ) ); - var ctorArg = n == 0 ? declaringMembers[0].Name : "f"; + var ctorArg = n == 0 ? declaringMembers[0].Identifier : "f"; if (n > 0) { @@ -794,9 +794,9 @@ public IEnumerable GenerateTableAccessors() "\n", autoIncFields.Select(m => $$""" - if (row.{{m.Name}} == default) + if (row.{{m.Identifier}} == default) { - row.{{m.Name}} = {{globalName}}.BSATN.{{m.Name}}{{TypeUse.BsatnFieldSuffix}}.Read(reader); + row.{{m.Identifier}} = {{globalName}}.BSATN.{{m.Identifier}}{{TypeUse.BsatnFieldSuffix}}.Read(reader); } """ ) @@ -904,8 +904,10 @@ string ColDecl(ColumnDeclaration col) var typeName = col.Type.Name; var isNullable = typeName.EndsWith("?", StringComparison.Ordinal); var valueTypeName = isNullable ? typeName[..^1] : typeName; - var colType = isNullable ? "global::SpacetimeDB.Col" : "global::SpacetimeDB.Col"; - return $"public readonly {colType}<{globalRowName}, {valueTypeName}> {col.Name};"; + var colType = isNullable + ? "global::SpacetimeDB.Col" + : "global::SpacetimeDB.Col"; + return $"public readonly {colType}<{globalRowName}, {valueTypeName}> {col.Identifier};"; } string ColInit(ColumnDeclaration col) @@ -913,8 +915,10 @@ string ColInit(ColumnDeclaration col) var typeName = col.Type.Name; var isNullable = typeName.EndsWith("?", StringComparison.Ordinal); var valueTypeName = isNullable ? typeName[..^1] : typeName; - var colType = isNullable ? "global::SpacetimeDB.Col" : "global::SpacetimeDB.Col"; - return $"{col.Name} = new {colType}<{globalRowName}, {valueTypeName}>(tableName, \"{col.Name}\");"; + var colType = isNullable + ? "global::SpacetimeDB.Col" + : "global::SpacetimeDB.Col"; + return $"{col.Identifier} = new {colType}<{globalRowName}, {valueTypeName}>(tableName, \"{col.Name}\");"; } var colsDecls = string.Join("\n ", Members.Select(ColDecl)); @@ -948,7 +952,7 @@ string IxColDecl(ColumnDeclaration col) var colType = isNullable ? "global::SpacetimeDB.IxCol" : "global::SpacetimeDB.IxCol"; - return $"public readonly {colType}<{globalRowName}, {valueTypeName}> {col.Name};"; + return $"public readonly {colType}<{globalRowName}, {valueTypeName}> {col.Identifier};"; } string IxColInit(ColumnDeclaration col) @@ -959,7 +963,7 @@ string IxColInit(ColumnDeclaration col) var colType = isNullable ? "global::SpacetimeDB.IxCol" : "global::SpacetimeDB.IxCol"; - return $"{col.Name} = new {colType}<{globalRowName}, {valueTypeName}>(tableName, \"{col.Name}\");"; + return $"{col.Identifier} = new {colType}<{globalRowName}, {valueTypeName}>(tableName, \"{col.Name}\");"; } var ixColsDecls = string.Join("\n ", ixMembers.Select(IxColDecl)); @@ -1251,7 +1255,7 @@ public string GenerateDispatcherClass(uint index) var paramReads = string.Join( "\n ", Parameters.Select(p => - $"var {p.Name} = {p.Name}{TypeUse.BsatnFieldSuffix}.Read(reader);" + $"var {p.Identifier} = {p.Identifier}{TypeUse.BsatnFieldSuffix}.Read(reader);" ) ); @@ -1305,7 +1309,9 @@ public string GenerateDispatcherClass(uint index) """; var invocationArgs = - Parameters.Length == 0 ? "" : ", " + string.Join(", ", Parameters.Select(p => p.Name)); + Parameters.Length == 0 + ? "" + : ", " + string.Join(", ", Parameters.Select(p => p.Identifier)); return $$$""" sealed class {{{Name}}}ViewDispatcher : {{{interfaceName}}} { {{{MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Parameters)}}} @@ -1392,7 +1398,8 @@ public string GenerateClass() ? "throw new System.InvalidOperationException()" : $"{FullName}({string.Join( ", ", - Args.Select(a => $"{a.Name}{TypeUse.BsatnFieldSuffix}.Read(reader)").Prepend("(SpacetimeDB.ReducerContext)ctx") + Args.Select(a => $"{a.Identifier}{TypeUse.BsatnFieldSuffix}.Read(reader)") + .Prepend("(SpacetimeDB.ReducerContext)ctx") )})"; return $$""" @@ -1435,13 +1442,13 @@ public Scope.Extensions GenerateSchedule() [System.Diagnostics.CodeAnalysis.Experimental("STDB_UNSTABLE")] public static void VolatileNonatomicScheduleImmediate{{Name}}({{string.Join( ", ", - Args.Select(a => $"{a.Type.Name} {a.Name}") + Args.Select(a => $"{a.Type.Name} {a.Identifier}") )}}) { using var stream = new MemoryStream(); using var writer = new BinaryWriter(stream); {{string.Join( "\n", - Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Name});") + Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Identifier});") )}} SpacetimeDB.Internal.IReducer.VolatileNonatomicScheduleImmediate(nameof({{Name}}), stream); } @@ -1539,7 +1546,9 @@ is INamedTypeSymbol public string GenerateClass() { var invocationArgs = - Args.Length == 0 ? "" : ", " + string.Join(", ", Args.Select(a => a.Name)); + Args.Length == 0 + ? "" + : ", " + string.Join(", ", Args.Select(a => a.Identifier)); var invocation = $"{FullName}((SpacetimeDB.ProcedureContext)ctx{invocationArgs})"; var txPayload = TxPayloadType ?? ReturnType; @@ -1601,7 +1610,7 @@ public string GenerateClass() : string.Join( "\n", Args.Select(a => - $" var {a.Name} = {a.Name}{TypeUse.BsatnFieldSuffix}.Read(reader);" + $" var {a.Identifier} = {a.Identifier}{TypeUse.BsatnFieldSuffix}.Read(reader);" ) ) + "\n"; @@ -1655,13 +1664,13 @@ public Scope.Extensions GenerateSchedule() [System.Diagnostics.CodeAnalysis.Experimental("STDB_UNSTABLE")] public static void VolatileNonatomicScheduleImmediate{{Name}}({{string.Join( ", ", - Args.Select(a => $"{a.Type.Name} {a.Name}") + Args.Select(a => $"{a.Type.Name} {a.Identifier}") )}}) { using var stream = new MemoryStream(); using var writer = new BinaryWriter(stream); {{string.Join( "\n", - Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Name});") + Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Identifier});") )}} SpacetimeDB.Internal.ProcedureExtensions.VolatileNonatomicScheduleImmediate(nameof({{Name}}), stream); } From e49364ad4c7fef373e8e5893f151b90269f32d38 Mon Sep 17 00:00:00 2001 From: Stable Genius <259448942+stablegenius49@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:27:41 -0800 Subject: [PATCH 2/2] Fix C# codegen keyword escaping (#4529) --- crates/bindings-csharp/BSATN.Codegen/Type.cs | 7 --- crates/bindings-csharp/BSATN.Codegen/Utils.cs | 13 +++++ crates/bindings-csharp/Codegen.Tests/Tests.cs | 25 ++++++++ crates/bindings-csharp/Codegen/Module.cs | 58 +++++++++++-------- 4 files changed, 73 insertions(+), 30 deletions(-) diff --git a/crates/bindings-csharp/BSATN.Codegen/Type.cs b/crates/bindings-csharp/BSATN.Codegen/Type.cs index 73e9be2f950..8897bbfc1c4 100644 --- a/crates/bindings-csharp/BSATN.Codegen/Type.cs +++ b/crates/bindings-csharp/BSATN.Codegen/Type.cs @@ -424,13 +424,6 @@ public MemberDeclaration(IFieldSymbol field, DiagReporter diag) public string Identifier => EscapeIdentifier(Name); - private static string EscapeIdentifier(string name) - { - var kind = SyntaxFacts.GetKeywordKind(name); - var contextualKind = SyntaxFacts.GetContextualKeywordKind(name); - return kind != SyntaxKind.None || contextualKind != SyntaxKind.None ? $"@{name}" : name; - } - public static string GenerateBsatnFields( Accessibility visibility, IEnumerable members diff --git a/crates/bindings-csharp/BSATN.Codegen/Utils.cs b/crates/bindings-csharp/BSATN.Codegen/Utils.cs index 71c1c5aea83..35ebab6f12b 100644 --- a/crates/bindings-csharp/BSATN.Codegen/Utils.cs +++ b/crates/bindings-csharp/BSATN.Codegen/Utils.cs @@ -33,6 +33,7 @@ public readonly record struct EquatableArray(ImmutableArray Array) : IEnum .AddMemberOptions(SymbolDisplayMemberOptions.IncludeContainingType) .AddMiscellaneousOptions( SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier + | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers ); public static string SymbolToName(ISymbol symbol) @@ -40,6 +41,18 @@ public static string SymbolToName(ISymbol symbol) return symbol.ToDisplayString(SymbolFormat); } + public static string EscapeIdentifier(string name) + { + if (name.Length > 0 && name[0] == '@') + { + return name; + } + + var kind = SyntaxFacts.GetKeywordKind(name); + var contextualKind = SyntaxFacts.GetContextualKeywordKind(name); + return kind != SyntaxKind.None || contextualKind != SyntaxKind.None ? $"@{name}" : name; + } + public static void RegisterSourceOutputs( this IncrementalValuesProvider methods, IncrementalGeneratorInitializationContext context diff --git a/crates/bindings-csharp/Codegen.Tests/Tests.cs b/crates/bindings-csharp/Codegen.Tests/Tests.cs index 1ad8c3eb463..86256bbff28 100644 --- a/crates/bindings-csharp/Codegen.Tests/Tests.cs +++ b/crates/bindings-csharp/Codegen.Tests/Tests.cs @@ -274,6 +274,21 @@ public partial struct KeywordTable public int @params; } + [SpacetimeDB.Table(Accessor = "class")] + public partial struct AccessorKeywordTable + { + [SpacetimeDB.PrimaryKey] + [SpacetimeDB.Index.BTree(Accessor = "class")] + public int Id; + } + + [SpacetimeDB.Table] + public partial struct @class + { + [SpacetimeDB.PrimaryKey] + public int Id; + } + public static partial class KeywordApis { [SpacetimeDB.Reducer] @@ -283,11 +298,21 @@ public static void KeywordReducer(ReducerContext ctx, int @params, string @class _ = @class; } + [SpacetimeDB.Reducer] + public static void @class(ReducerContext ctx) + { + } + [SpacetimeDB.Procedure] public static int KeywordProcedure(ProcedureContext ctx, int @params, int @class) { return @params + @class; } + + [SpacetimeDB.Procedure] + public static void @params(ProcedureContext ctx) + { + } } """; diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index 1fec8a059ec..9dc8a106462 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -275,6 +275,8 @@ record TableAccessor public readonly bool IsEvent; public readonly Scheduled? Scheduled; + public string Identifier => EscapeIdentifier(Name); + public TableAccessor(TableDeclaration table, AttributeData data, DiagReporter diag) { var attr = data.ParseAs(); @@ -334,6 +336,8 @@ record TableIndex public readonly string? CanonicalName; public readonly TableIndexType Type; + public string AccessorIdentifier => EscapeIdentifier(AccessorName); + // See: bindings_sys::index_id_from_name for documentation of this format. // Guaranteed not to contain quotes, so does not need to be escaped when embedded in a string. private readonly string StandardNameSuffix; @@ -574,7 +578,7 @@ public IEnumerable GenerateTableAccessorFilters(TableAccessor tableAcces ? $"public {globalName} Update({globalName} row) => DoUpdate(row);" : ""; yield return $$""" - {{vis}} sealed class {{f.Identifier}}UniqueIndex : {{uniqueIndexBase}}<{{tableAccessor.Name}}, {{globalName}}, {{f.Type.Name}}, {{f.Type.BSATNName}}> { + {{vis}} sealed class {{f.Identifier}}UniqueIndex : {{uniqueIndexBase}}<{{tableAccessor.Identifier}}, {{globalName}}, {{f.Type.Name}}, {{f.Type.BSATNName}}> { internal {{f.Identifier}}UniqueIndex() : base("{{standardIndexName}}") {} // Important: don't move this to the base class. // C# generics don't play well with nullable types and can't accept both struct-type-based and class-type-based @@ -589,6 +593,7 @@ public IEnumerable GenerateTableAccessorFilters(TableAccessor tableAcces foreach (var index in GetIndexes(tableAccessor)) { var name = index.AccessorName; + var identifierName = index.AccessorIdentifier; // Skip bad declarations. Empty name means no columns, which we have already reported with a meaningful error. // Emitting this will result in further compilation errors due to missing property name. @@ -601,7 +606,7 @@ public IEnumerable GenerateTableAccessorFilters(TableAccessor tableAcces var standardIndexName = index.StandardIndexName(tableAccessor); yield return $$""" - {{vis}} sealed class {{name}}Index() : SpacetimeDB.Internal.IndexBase<{{globalName}}>("{{standardIndexName}}") { + {{vis}} sealed class {{identifierName}}Index() : SpacetimeDB.Internal.IndexBase<{{globalName}}>("{{standardIndexName}}") { """; for (var n = 0; n < members.Length; n++) @@ -644,7 +649,7 @@ public ulong Delete({{argsBounds}}) => """; } - yield return $"}}\n {vis} {name}Index {name} => new();\n"; + yield return $"}}\n {vis} {identifierName}Index {identifierName} => new();\n"; } } @@ -670,7 +675,7 @@ private IEnumerable GenerateReadOnlyAccessorFilters(TableAccessor tableA yield return $$$""" public sealed class {{{f.Identifier}}}Index : {{{uniqueIndexBase}}}< - global::SpacetimeDB.Internal.ViewHandles.{{{tableAccessor.Name}}}ReadOnly, + global::SpacetimeDB.Internal.ViewHandles.{{{tableAccessor.Identifier}}}ReadOnly, {{{globalName}}}, {{{f.Type.Name}}}, {{{f.Type.BSATNName}}}> @@ -783,12 +788,13 @@ public IEnumerable GenerateTableAccessors() var autoIncFields = Members.Where(m => m.GetAttrs(v).HasFlag(ColumnAttrs.AutoInc)); var globalName = $"global::{FullName}"; - var iTable = $"global::SpacetimeDB.Internal.ITableView<{v.Name}, {globalName}>"; + var accessorIdentifier = v.Identifier; + var iTable = $"global::SpacetimeDB.Internal.ITableView<{accessorIdentifier}, {globalName}>"; yield return new( v.Name, globalName, $$$""" - {{{SyntaxFacts.GetText(Visibility)}}} readonly struct {{{v.Name}}} : {{{iTable}}} { + {{{SyntaxFacts.GetText(Visibility)}}} readonly struct {{{accessorIdentifier}}} : {{{iTable}}} { public static {{{globalName}}} ReadGenFields(System.IO.BinaryReader reader, {{{globalName}}} row) { {{{string.Join( "\n", @@ -805,7 +811,7 @@ public IEnumerable GenerateTableAccessors() } public static SpacetimeDB.Internal.RawTableDefV10 MakeTableDesc(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new ( - SourceName: nameof({{{v.Name}}}), + SourceName: nameof({{{accessorIdentifier}}}), ProductTypeRef: (uint) new {{{globalName}}}.BSATN().GetAlgebraicType(registrar).Ref_, PrimaryKey: [{{{GetPrimaryKey(v)?.ToString() ?? ""}}}], Indexes: [ @@ -839,7 +845,7 @@ v.Scheduled is { } scheduled {{{string.Join("\n", GenerateTableAccessorFilters(v))}}} } """, - $"{SyntaxFacts.GetText(Visibility)} global::SpacetimeDB.Internal.TableHandles.{v.Name} {v.Name} => new();" + $"{SyntaxFacts.GetText(Visibility)} global::SpacetimeDB.Internal.TableHandles.{accessorIdentifier} {accessorIdentifier} => new();" ); } } @@ -861,6 +867,7 @@ public IEnumerable GenerateReadOnlyAccessors() foreach (var accessor in TableAccessors) { var globalName = $"global::{FullName}"; + var accessorIdentifier = accessor.Identifier; var readOnlyIndexDecls = string.Join("\n", GenerateReadOnlyAccessorFilters(accessor)); var visibility = SyntaxFacts.GetText(Visibility); @@ -868,17 +875,17 @@ public IEnumerable GenerateReadOnlyAccessors() accessor.Name, globalName, $$$""" - {{{visibility}}} sealed class {{{accessor.Name}}}ReadOnly + {{{visibility}}} sealed class {{{accessorIdentifier}}}ReadOnly : global::SpacetimeDB.Internal.ReadOnlyTableView<{{{globalName}}}> { - internal {{{accessor.Name}}}ReadOnly() : base("{{{accessor.Name}}}") { } + internal {{{accessorIdentifier}}}ReadOnly() : base("{{{accessor.Name}}}") { } public ulong Count => DoCount(); {{{readOnlyIndexDecls}}} } """, - $"{visibility} global::SpacetimeDB.Internal.ViewHandles.{accessor.Name}ReadOnly {accessor.Name} => new();" + $"{visibility} global::SpacetimeDB.Internal.ViewHandles.{accessorIdentifier}ReadOnly {accessorIdentifier} => new();" ); } } @@ -895,9 +902,10 @@ public IEnumerable GenerateQueryBuilderMembers() foreach (var accessor in TableAccessors) { + var accessorIdentifier = accessor.Identifier; var tableName = accessor.Name; - var colsTypeName = $"{accessor.Name}Cols"; - var ixColsTypeName = $"{accessor.Name}IxCols"; + var colsTypeName = $"{accessorIdentifier}Cols"; + var ixColsTypeName = $"{accessorIdentifier}IxCols"; string ColDecl(ColumnDeclaration col) { @@ -992,7 +1000,7 @@ string IxColInit(ColumnDeclaration col) public readonly partial struct QueryBuilder { - {{vis}} global::SpacetimeDB.Table<{{globalRowName}}, {{colsTypeName}}, {{ixColsTypeName}}> {{accessor.Name}}() => + {{vis}} global::SpacetimeDB.Table<{{globalRowName}}, {{colsTypeName}}, {{ixColsTypeName}}> {{accessorIdentifier}}() => new("{{tableName}}", new {{colsTypeName}}("{{tableName}}"), new {{ixColsTypeName}}("{{tableName}}")); } """; @@ -1350,6 +1358,8 @@ record ReducerDeclaration public readonly Scope Scope; private readonly bool HasWrongSignature; + public string Identifier => EscapeIdentifier(Name); + public ReducerDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter diag) { var methodSyntax = (MethodDeclarationSyntax)context.TargetNode; @@ -1403,11 +1413,11 @@ public string GenerateClass() )})"; return $$""" - class {{Name}}: SpacetimeDB.Internal.IReducer { + class {{Identifier}}: SpacetimeDB.Internal.IReducer { {{MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Args)}} public SpacetimeDB.Internal.RawReducerDefV10 MakeReducerDef(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new ( - SourceName: nameof({{Name}}), + SourceName: nameof({{Identifier}}), Params: [{{MemberDeclaration.GenerateDefs(Args)}}], Visibility: SpacetimeDB.Internal.FunctionVisibility.ClientCallable, OkReturnType: SpacetimeDB.BSATN.AlgebraicType.Unit, @@ -1450,7 +1460,7 @@ public Scope.Extensions GenerateSchedule() "\n", Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Identifier});") )}} - SpacetimeDB.Internal.IReducer.VolatileNonatomicScheduleImmediate(nameof({{Name}}), stream); + SpacetimeDB.Internal.IReducer.VolatileNonatomicScheduleImmediate(nameof({{Identifier}}), stream); } """ ); @@ -1475,6 +1485,8 @@ record ProcedureDeclaration private readonly TypeUse? TxPayloadType; private readonly bool TxPayloadIsUnit; + public string Identifier => EscapeIdentifier(Name); + public ProcedureDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter diag) { var methodSyntax = (MethodDeclarationSyntax)context.TargetNode; @@ -1634,11 +1646,11 @@ public string GenerateClass() } return $$$""" - class {{{Name}}} : SpacetimeDB.Internal.IProcedure { + class {{{Identifier}}} : SpacetimeDB.Internal.IProcedure { {{{classFields}}} public SpacetimeDB.Internal.RawProcedureDefV10 MakeProcedureDef(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new( - SourceName: nameof({{{Name}}}), + SourceName: nameof({{{Identifier}}}), Params: [{{{MemberDeclaration.GenerateDefs(Args)}}}], ReturnType: {{{returnTypeExpr}}}, Visibility: SpacetimeDB.Internal.FunctionVisibility.ClientCallable @@ -1672,7 +1684,7 @@ public Scope.Extensions GenerateSchedule() "\n", Args.Select(a => $"new {a.Type.ToBSATNString()}().Write(writer, {a.Identifier});") )}} - SpacetimeDB.Internal.ProcedureExtensions.VolatileNonatomicScheduleImmediate(nameof({{Name}}), stream); + SpacetimeDB.Internal.ProcedureExtensions.VolatileNonatomicScheduleImmediate(nameof({{Identifier}}), stream); } """ ); @@ -2340,13 +2352,13 @@ public static void Main() { {{string.Join( "\n", addReducers.Select(r => - $"SpacetimeDB.Internal.Module.RegisterReducer<{r.Name}>();" + $"SpacetimeDB.Internal.Module.RegisterReducer<{EscapeIdentifier(r.Name)}>();" ) )}} {{string.Join( "\n", addProcedures.Select(r => - $"SpacetimeDB.Internal.Module.RegisterProcedure<{r.Name}>();" + $"SpacetimeDB.Internal.Module.RegisterProcedure<{EscapeIdentifier(r.Name)}>();" ) )}} @@ -2364,7 +2376,7 @@ public static void Main() { {{string.Join( "\n", - tableAccessors.Select(t => $"SpacetimeDB.Internal.Module.RegisterTable<{t.tableName}, SpacetimeDB.Internal.TableHandles.{t.tableAccessorName}>();") + tableAccessors.Select(t => $"SpacetimeDB.Internal.Module.RegisterTable<{t.tableName}, SpacetimeDB.Internal.TableHandles.{EscapeIdentifier(t.tableAccessorName)}>();") )}} {{string.Join( "\n",