diff --git a/crates/bindings-csharp/Codegen.Tests/Tests.cs b/crates/bindings-csharp/Codegen.Tests/Tests.cs index 8f66461c837..3ff8da9b61f 100644 --- a/crates/bindings-csharp/Codegen.Tests/Tests.cs +++ b/crates/bindings-csharp/Codegen.Tests/Tests.cs @@ -221,6 +221,23 @@ public static async Task TypeAndModuleGeneratorsOnServer() AssertNoCs0436Diagnostics(compilationWithUserCode); } + [Fact] + public static async Task SettingsAndExplicitNames() + { + var fixture = await Fixture.Compile("explicitnames"); + + var compilationAfterGen = await fixture.RunAndCheckGenerators( + new SpacetimeDB.Codegen.Type(), + new SpacetimeDB.Codegen.Module() + ); + + Assert.Empty(GetCompilationErrors(compilationAfterGen)); + + AssertPublicBoundIsAvailableInRuntime(compilationAfterGen); + AssertRuntimeDoesNotDefineLocal(compilationAfterGen); + AssertGeneratedCodeDoesNotUseInternalBound(compilationAfterGen); + } + [Fact] public static async Task TestDiagnostics() { diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs index 4cc1d7feded..df3d285d793 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs @@ -873,8 +873,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "Identity", + SourceName: "Player_Identity_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ) ], @@ -956,8 +956,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "IdentityField", + SourceName: "TestAutoIncNotInteger_IdentityField_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1]) ) ], @@ -1056,8 +1056,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "UniqueField", + SourceName: "TestDefaultFieldValues_UniqueField_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ) ], @@ -1182,23 +1182,23 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "TestIndexWithoutColumns", + SourceName: "TestIndexIssues__idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([]) ), new( - SourceName: null, - AccessorName: "TestIndexWithEmptyColumns", + SourceName: "TestIndexIssues__idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([]) ), new( - SourceName: null, - AccessorName: "TestUnknownColumns", + SourceName: "TestIndexIssues__idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([]) ), new( - SourceName: null, - AccessorName: "TestUnexpectedColumns", + SourceName: "TestIndexIssues_SelfIndexingColumn_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ) ], @@ -1441,8 +1441,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "IdCorrectType", + SourceName: "TestScheduleWithoutScheduleAt_IdCorrectType_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1]) ) ], @@ -1534,8 +1534,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "IdWrongType", + SourceName: "TestScheduleWithWrongPrimaryKeyType_IdWrongType_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ) ], @@ -1631,8 +1631,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "IdCorrectType", + SourceName: "TestScheduleWithWrongScheduleAtType_IdCorrectType_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1]) ) ], @@ -1728,13 +1728,13 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "UniqueField", + SourceName: "TestUniqueNotEquatable_UniqueField_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ), new( - SourceName: null, - AccessorName: "PrimaryKeyField", + SourceName: "TestUniqueNotEquatable_PrimaryKeyField_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1]) ) ], diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/Lib.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/Lib.cs new file mode 100644 index 00000000000..6407c78dea9 --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/Lib.cs @@ -0,0 +1,46 @@ +using SpacetimeDB; + +#pragma warning disable CA1050 // Declare types in namespaces - this is a test fixture, no need for a namespace. + +public static partial class Module +{ + [SpacetimeDB.Settings] + public const SpacetimeDB.Internal.CaseConversionPolicy CASE_CONVERSION_POLICY = SpacetimeDB + .Internal + .CaseConversionPolicy + .SnakeCase; +} + +[SpacetimeDB.Type] +public partial struct DemoType +{ + public int A; +} + +[SpacetimeDB.Table(Accessor = "DemoTable", Name = "canonical_table", Public = true)] +public partial struct DemoTable +{ + [SpacetimeDB.PrimaryKey] + [SpacetimeDB.Index.BTree(Accessor = "ById", Name = "canonical_index")] + public int Id; + + public int Value; +} + +public static partial class Reducers +{ + [SpacetimeDB.Reducer(Name = "canonical_reducer")] + public static void DemoReducer(ReducerContext ctx, int value) + { + ctx.Db.DemoTable.Insert(new DemoTable { Id = value, Value = value }); + } + + [SpacetimeDB.Procedure(Name = "canonical_procedure")] + public static void DemoProcedure(ProcedureContext ctx) { } + + [SpacetimeDB.View(Accessor = "demo_view", Name = "canonical_view", Public = true)] + public static List DemoView(ViewContext ctx) + { + return new List(); + } +} diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/explicitnames.csproj b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/explicitnames.csproj new file mode 100644 index 00000000000..65397bf9f03 --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/explicitnames.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + + + + + + + + diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#DemoTable.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#DemoTable.verified.cs new file mode 100644 index 00000000000..c43a88b3f68 --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#DemoTable.verified.cs @@ -0,0 +1,107 @@ +//HintName: DemoTable.cs +// +#nullable enable + +partial struct DemoTable : System.IEquatable, SpacetimeDB.BSATN.IStructuralReadWrite +{ + public void ReadFields(System.IO.BinaryReader reader) + { + Id = BSATN.IdRW.Read(reader); + Value = BSATN.ValueRW.Read(reader); + } + + public void WriteFields(System.IO.BinaryWriter writer) + { + BSATN.IdRW.Write(writer, Id); + BSATN.ValueRW.Write(writer, Value); + } + + object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() + { + return new BSATN(); + } + + public override string ToString() => + $"DemoTable {{ Id = {SpacetimeDB.BSATN.StringUtil.GenericToString(Id)}, Value = {SpacetimeDB.BSATN.StringUtil.GenericToString(Value)} }}"; + + public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite + { + internal static readonly SpacetimeDB.BSATN.I32 IdRW = new(); + internal static readonly SpacetimeDB.BSATN.I32 ValueRW = new(); + + public DemoTable Read(System.IO.BinaryReader reader) + { + var ___result = new DemoTable(); + ___result.ReadFields(reader); + return ___result; + } + + public void Write(System.IO.BinaryWriter writer, DemoTable value) + { + value.WriteFields(writer); + } + + public SpacetimeDB.BSATN.AlgebraicType.Ref GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => + registrar.RegisterType(_ => new SpacetimeDB.BSATN.AlgebraicType.Product( + new SpacetimeDB.BSATN.AggregateElement[] + { + new("Id", IdRW.GetAlgebraicType(registrar)), + new("Value", ValueRW.GetAlgebraicType(registrar)) + } + )); + + SpacetimeDB.BSATN.AlgebraicType SpacetimeDB.BSATN.IReadWrite.GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => GetAlgebraicType(registrar); + } + + public override int GetHashCode() + { + var ___hashId = Id.GetHashCode(); + var ___hashValue = Value.GetHashCode(); + return ___hashId ^ ___hashValue; + } + +#nullable enable + public bool Equals(DemoTable that) + { + var ___eqId = this.Id.Equals(that.Id); + var ___eqValue = this.Value.Equals(that.Value); + return ___eqId && ___eqValue; + } + + public override bool Equals(object? that) + { + if (that == null) + { + return false; + } + var that_ = that as DemoTable?; + if (((object?)that_) == null) + { + return false; + } + return Equals(that_); + } + + public static bool operator ==(DemoTable this_, DemoTable that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return object.Equals(this_, that); + } + return this_.Equals(that); + } + + public static bool operator !=(DemoTable this_, DemoTable that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return !object.Equals(this_, that); + } + return !this_.Equals(that); + } +#nullable restore +} // DemoTable diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#FFI.verified.cs new file mode 100644 index 00000000000..68c34fb1dce --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#FFI.verified.cs @@ -0,0 +1,648 @@ +//HintName: FFI.cs +// +#nullable enable +// The runtime already defines SpacetimeDB.Internal.LocalReadOnly in Runtime\Internal\Module.cs as an empty partial type. +// This is needed so every module build doesn't generate a full LocalReadOnly type, but just adds on to the existing. +// We extend it here with generated table accessors, and just need to suppress the duplicate-type warning. +#pragma warning disable CS0436 +#pragma warning disable STDB_UNSTABLE + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Internal = SpacetimeDB.Internal; +using TxContext = SpacetimeDB.Internal.TxContext; + +namespace SpacetimeDB +{ + public readonly struct DemoTableCols + { + public readonly global::SpacetimeDB.Col Id; + public readonly global::SpacetimeDB.Col Value; + + internal DemoTableCols(string tableName) + { + Id = new global::SpacetimeDB.Col(tableName, "Id"); + Value = new global::SpacetimeDB.Col(tableName, "Value"); + } + } + + public readonly struct DemoTableIxCols + { + public readonly global::SpacetimeDB.IxCol Id; + + internal DemoTableIxCols(string tableName) + { + Id = new global::SpacetimeDB.IxCol(tableName, "Id"); + } + } + + public readonly partial struct QueryBuilder + { + public global::SpacetimeDB.Table< + global::DemoTable, + DemoTableCols, + DemoTableIxCols + > DemoTable() => + new("DemoTable", new DemoTableCols("DemoTable"), new DemoTableIxCols("DemoTable")); + } + + public sealed record ReducerContext : DbContext, Internal.IReducerContext + { + public readonly Identity Sender; + public readonly ConnectionId? ConnectionId; + public readonly Random Rng; + public readonly Timestamp Timestamp; + public readonly AuthCtx SenderAuth; + + // **Note:** must be 0..=u32::MAX + internal int CounterUuid; + + // We need this property to be non-static for parity with client SDK. + public Identity Identity => Internal.IReducerContext.GetIdentity(); + + internal ReducerContext( + Identity identity, + ConnectionId? connectionId, + Random random, + Timestamp time, + AuthCtx? senderAuth = null + ) + { + Sender = identity; + ConnectionId = connectionId; + Rng = random; + Timestamp = time; + SenderAuth = senderAuth ?? AuthCtx.BuildFromSystemTables(connectionId, identity); + CounterUuid = 0; + } + + /// + /// Create a new random `v4` using the built-in RNG. + /// + /// + /// This method fills the random bytes using the context RNG. + /// + /// + /// + /// var uuid = ctx.NewUuidV4(); + /// Log.Info(uuid); + /// + /// + public Uuid NewUuidV4() + { + var bytes = new byte[16]; + Rng.NextBytes(bytes); + return Uuid.FromRandomBytesV4(bytes); + } + + /// + /// Create a new sortable `v7` using the built-in RNG, monotonic counter, + /// and timestamp. + /// + /// + /// A newly generated `v7` that is monotonically ordered + /// and suitable for use as a primary key or for ordered storage. + /// + /// + /// Thrown if generation fails. + /// + /// + /// + /// [SpacetimeDB.Reducer] + /// public static Guid GenerateUuidV7(ReducerContext ctx) + /// { + /// Guid uuid = ctx.NewUuidV7(); + /// Log.Info(uuid); + /// } + /// + /// + public Uuid NewUuidV7() + { + var bytes = new byte[4]; + Rng.NextBytes(bytes); + return Uuid.FromCounterV7(ref CounterUuid, Timestamp, bytes); + } + } + + public sealed partial class ProcedureContext : global::SpacetimeDB.ProcedureContextBase + { + private readonly Local _db = new(); + + internal ProcedureContext( + Identity identity, + ConnectionId? connectionId, + Random random, + Timestamp time + ) + : base(identity, connectionId, random, time) { } + + protected override global::SpacetimeDB.LocalBase CreateLocal() => _db; + + protected override global::SpacetimeDB.ProcedureTxContextBase CreateTxContext( + Internal.TxContext inner + ) => _cached ??= new ProcedureTxContext(inner); + + private ProcedureTxContext? _cached; + + [Experimental("STDB_UNSTABLE")] + public Local Db => _db; + + [Experimental("STDB_UNSTABLE")] + public TResult WithTx(Func body) => + base.WithTx(tx => body((ProcedureTxContext)tx)); + + [Experimental("STDB_UNSTABLE")] + public TxOutcome TryWithTx( + Func> body + ) + where TError : Exception => base.TryWithTx(tx => body((ProcedureTxContext)tx)); + + /// + /// Create a new random `v4` using the built-in RNG. + /// + /// + /// This method fills the random bytes using the context RNG. + /// + /// + /// + /// var uuid = ctx.NewUuidV4(); + /// Log.Info(uuid); + /// + /// + public Uuid NewUuidV4() + { + var bytes = new byte[16]; + Rng.NextBytes(bytes); + return Uuid.FromRandomBytesV4(bytes); + } + + /// + /// Create a new sortable `v7` using the built-in RNG, monotonic counter, + /// and timestamp. + /// + /// + /// A newly generated `v7` that is monotonically ordered + /// and suitable for use as a primary key or for ordered storage. + /// + /// + /// Thrown if UUID generation fails. + /// + /// + /// + /// [SpacetimeDB.Procedure] + /// public static Guid GenerateUuidV7(ReducerContext ctx) + /// { + /// Guid uuid = ctx.NewUuidV7(); + /// Log.Info(uuid); + /// } + /// + /// + public Uuid NewUuidV7() + { + var bytes = new byte[4]; + Rng.NextBytes(bytes); + return Uuid.FromCounterV7(ref CounterUuid, Timestamp, bytes); + } + } + + [Experimental("STDB_UNSTABLE")] + public sealed class ProcedureTxContext : global::SpacetimeDB.ProcedureTxContextBase + { + internal ProcedureTxContext(Internal.TxContext inner) + : base(inner) { } + + public new Local Db => (Local)base.Db; + } + + public sealed class Local : global::SpacetimeDB.LocalBase + { + public global::SpacetimeDB.Internal.TableHandles.DemoTable DemoTable => new(); + } + + public sealed record ViewContext : DbContext, Internal.IViewContext + { + public Identity Sender { get; } + + public QueryBuilder From => default; + + internal ViewContext(Identity sender, Internal.LocalReadOnly db) + : base(db) + { + Sender = sender; + } + } + + public sealed record AnonymousViewContext + : DbContext, + Internal.IAnonymousViewContext + { + public QueryBuilder From => default; + + internal AnonymousViewContext(Internal.LocalReadOnly db) + : base(db) { } + } +} + +namespace SpacetimeDB.Internal.TableHandles +{ + public readonly struct DemoTable + : global::SpacetimeDB.Internal.ITableView + { + public static global::DemoTable ReadGenFields( + System.IO.BinaryReader reader, + global::DemoTable row + ) + { + return row; + } + + public static SpacetimeDB.Internal.RawTableDefV10 MakeTableDesc( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => + new( + SourceName: nameof(DemoTable), + ProductTypeRef: (uint) + new global::DemoTable.BSATN().GetAlgebraicType(registrar).Ref_, + PrimaryKey: [0], + Indexes: + [ + new( + SourceName: "DemoTable_Id_idx_btree", + AccessorName: null, + Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) + ), + new( + SourceName: "DemoTable_Id_idx_btree", + AccessorName: null, + Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) + ) + ], + Constraints: + [ + global::SpacetimeDB.Internal.ITableView< + DemoTable, + global::DemoTable + >.MakeUniqueConstraint(0) + ], + Sequences: [], + TableType: SpacetimeDB.Internal.TableType.User, + TableAccess: SpacetimeDB.Internal.TableAccess.Public, + DefaultValues: [], + IsEvent: false + ); + + public static SpacetimeDB.Internal.RawScheduleDefV10? MakeScheduleDesc() => null; + + public ulong Count => + global::SpacetimeDB.Internal.ITableView.DoCount(); + + public IEnumerable Iter() => + global::SpacetimeDB.Internal.ITableView.DoIter(); + + public global::DemoTable Insert(global::DemoTable row) => + global::SpacetimeDB.Internal.ITableView.DoInsert(row); + + public bool Delete(global::DemoTable row) => + global::SpacetimeDB.Internal.ITableView.DoDelete(row); + + public sealed class IdUniqueIndex + : UniqueIndex + { + internal IdUniqueIndex() + : base("DemoTable_Id_idx_btree") { } + + // 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 global::DemoTable? Find(int key) => FindSingle(key); + + public global::DemoTable Update(global::DemoTable row) => DoUpdate(row); + } + + public IdUniqueIndex Id => new(); + + public sealed class ByIdIndex() + : SpacetimeDB.Internal.IndexBase("DemoTable_Id_idx_btree") + { + public IEnumerable Filter(int Id) => + DoFilter(new SpacetimeDB.Internal.BTreeIndexBounds(Id)); + + public ulong Delete(int Id) => + DoDelete(new SpacetimeDB.Internal.BTreeIndexBounds(Id)); + + public IEnumerable Filter(global::SpacetimeDB.Bound Id) => + DoFilter(new SpacetimeDB.Internal.BTreeIndexBounds(Id)); + + public ulong Delete(global::SpacetimeDB.Bound Id) => + DoDelete(new SpacetimeDB.Internal.BTreeIndexBounds(Id)); + } + + public ByIdIndex ById => new(); + } +} + +sealed class demo_viewViewDispatcher : global::SpacetimeDB.Internal.IView +{ + public SpacetimeDB.Internal.RawViewDefV10 MakeViewDef( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => + new global::SpacetimeDB.Internal.RawViewDefV10( + SourceName: "demo_view", + Index: 0, + IsPublic: true, + IsAnonymous: false, + Params: [], + ReturnType: new SpacetimeDB.BSATN.List().GetAlgebraicType( + registrar + ) + ); + + public byte[] Invoke( + System.IO.BinaryReader reader, + global::SpacetimeDB.Internal.IViewContext ctx + ) + { + try + { + var returnValue = Reducers.DemoView((SpacetimeDB.ViewContext)ctx); + SpacetimeDB.BSATN.List returnRW = new(); + var header = new global::SpacetimeDB.Internal.ViewResultHeader.RowData(default); + var headerRW = new global::SpacetimeDB.Internal.ViewResultHeader.BSATN(); + using var output = new System.IO.MemoryStream(); + using var writer = new System.IO.BinaryWriter(output); + headerRW.Write(writer, header); + returnRW.Write(writer, returnValue); + return output.ToArray(); + } + catch (System.Exception e) + { + global::SpacetimeDB.Log.Error("Error in view 'demo_view': " + e); + throw; + } + } +} + +namespace SpacetimeDB.Internal.ViewHandles +{ + public sealed class DemoTableReadOnly + : global::SpacetimeDB.Internal.ReadOnlyTableView + { + internal DemoTableReadOnly() + : base("DemoTable") { } + + public ulong Count => DoCount(); + + public sealed class IdIndex + : global::SpacetimeDB.Internal.ReadOnlyUniqueIndex< + global::SpacetimeDB.Internal.ViewHandles.DemoTableReadOnly, + global::DemoTable, + int, + SpacetimeDB.BSATN.I32 + > + { + internal IdIndex() + : base("DemoTable_Id_idx_btree") { } + + public global::DemoTable? Find(int key) => FindSingle(key); + } + + public IdIndex Id => new(); + + public sealed class ByIdIndex + : global::SpacetimeDB.Internal.ReadOnlyIndexBase + { + internal ByIdIndex() + : base("DemoTable_Id_idx_btree") { } + + public IEnumerable Filter(int Id) => + DoFilter( + new global::SpacetimeDB.Internal.BTreeIndexBounds( + Id + ) + ); + + public IEnumerable Filter(global::SpacetimeDB.Bound Id) => + DoFilter( + new global::SpacetimeDB.Internal.BTreeIndexBounds( + Id + ) + ); + } + + public ByIdIndex ById => new(); + } +} + +namespace SpacetimeDB.Internal +{ + public sealed partial class LocalReadOnly + { + public global::SpacetimeDB.Internal.ViewHandles.DemoTableReadOnly DemoTable => new(); + } +} + +static class ModuleRegistration +{ + class DemoReducer : SpacetimeDB.Internal.IReducer + { + private static readonly SpacetimeDB.BSATN.I32 valueRW = new(); + + public SpacetimeDB.Internal.RawReducerDefV10 MakeReducerDef( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => + new( + SourceName: nameof(DemoReducer), + Params: [new("value", valueRW.GetAlgebraicType(registrar))], + Visibility: SpacetimeDB.Internal.FunctionVisibility.ClientCallable, + OkReturnType: SpacetimeDB.BSATN.AlgebraicType.Unit, + ErrReturnType: new SpacetimeDB.BSATN.AlgebraicType.String(default) + ); + + public SpacetimeDB.Internal.Lifecycle? Lifecycle => null; + + public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx) + { + Reducers.DemoReducer((SpacetimeDB.ReducerContext)ctx, valueRW.Read(reader)); + } + } + + class DemoProcedure : SpacetimeDB.Internal.IProcedure + { + public SpacetimeDB.Internal.RawProcedureDefV10 MakeProcedureDef( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => + new( + SourceName: nameof(DemoProcedure), + Params: [], + ReturnType: SpacetimeDB.BSATN.AlgebraicType.Unit, + Visibility: SpacetimeDB.Internal.FunctionVisibility.ClientCallable + ); + + public byte[] Invoke(BinaryReader reader, SpacetimeDB.Internal.IProcedureContext ctx) + { + Reducers.DemoProcedure((SpacetimeDB.ProcedureContext)ctx); + return System.Array.Empty(); + } + } + + public static List ToListOrEmpty(T? value) + where T : struct => value is null ? new List() : new List { value.Value }; + + public static List ToListOrEmpty(T? value) + where T : class => value is null ? new List() : new List { value }; + +#if EXPERIMENTAL_WASM_AOT + // In AOT mode we're building a library. + // Main method won't be called automatically, so we need to export it as a preinit function. + [UnmanagedCallersOnly(EntryPoint = "__preinit__10_init_csharp")] +#else + // Prevent trimming of FFI exports that are invoked from C and not visible to C# trimmer. + [DynamicDependency( + DynamicallyAccessedMemberTypes.PublicMethods, + typeof(SpacetimeDB.Internal.Module) + )] +#endif + public static void Main() + { + SpacetimeDB.Internal.Module.SetReducerContextConstructor( + (identity, connectionId, random, time) => + new SpacetimeDB.ReducerContext(identity, connectionId, random, time) + ); + SpacetimeDB.Internal.Module.SetViewContextConstructor( + identity => new SpacetimeDB.ViewContext( + identity, + new SpacetimeDB.Internal.LocalReadOnly() + ) + ); + SpacetimeDB.Internal.Module.SetAnonymousViewContextConstructor( + () => new SpacetimeDB.AnonymousViewContext(new SpacetimeDB.Internal.LocalReadOnly()) + ); + SpacetimeDB.Internal.Module.SetProcedureContextConstructor( + (identity, connectionId, random, time) => + new SpacetimeDB.ProcedureContext(identity, connectionId, random, time) + ); + SpacetimeDB.Internal.Module.SetCaseConversionPolicy( + SpacetimeDB.Internal.CaseConversionPolicy.SnakeCase + ); + SpacetimeDB.Internal.Module.RegisterExplicitTableName("DemoTable", "canonical_table"); + SpacetimeDB.Internal.Module.RegisterExplicitFunctionName( + "DemoReducer", + "canonical_reducer" + ); + SpacetimeDB.Internal.Module.RegisterExplicitFunctionName( + "DemoProcedure", + "canonical_procedure" + ); + SpacetimeDB.Internal.Module.RegisterExplicitFunctionName("demo_view", "canonical_view"); + SpacetimeDB.Internal.Module.RegisterExplicitIndexName( + "DemoTable_Id_idx_btree", + "canonical_index" + ); + + var __memoryStream = new MemoryStream(); + var __writer = new BinaryWriter(__memoryStream); + + SpacetimeDB.Internal.Module.RegisterReducer(); + SpacetimeDB.Internal.Module.RegisterProcedure(); + + // IMPORTANT: The order in which we register views matters. + // It must correspond to the order in which we call `GenerateDispatcherClass`. + // See the comment on `GenerateDispatcherClass` for more explanation. + SpacetimeDB.Internal.Module.RegisterView(); + + SpacetimeDB.Internal.Module.RegisterTable< + global::DemoTable, + SpacetimeDB.Internal.TableHandles.DemoTable + >(); + } + + // Exports only work from the main assembly, so we need to generate forwarding methods. +#if EXPERIMENTAL_WASM_AOT + [UnmanagedCallersOnly(EntryPoint = "__describe_module__")] + public static void __describe_module__(SpacetimeDB.Internal.BytesSink d) => + SpacetimeDB.Internal.Module.__describe_module__(d); + + [UnmanagedCallersOnly(EntryPoint = "__call_reducer__")] + public static SpacetimeDB.Internal.Errno __call_reducer__( + uint id, + ulong sender_0, + ulong sender_1, + ulong sender_2, + ulong sender_3, + ulong conn_id_0, + ulong conn_id_1, + SpacetimeDB.Timestamp timestamp, + SpacetimeDB.Internal.BytesSource args, + SpacetimeDB.Internal.BytesSink error + ) => + SpacetimeDB.Internal.Module.__call_reducer__( + id, + sender_0, + sender_1, + sender_2, + sender_3, + conn_id_0, + conn_id_1, + timestamp, + args, + error + ); + + [UnmanagedCallersOnly(EntryPoint = "__call_procedure__")] + public static SpacetimeDB.Internal.Errno __call_procedure__( + uint id, + ulong sender_0, + ulong sender_1, + ulong sender_2, + ulong sender_3, + ulong conn_id_0, + ulong conn_id_1, + SpacetimeDB.Timestamp timestamp, + SpacetimeDB.Internal.BytesSource args, + SpacetimeDB.Internal.BytesSink result_sink + ) => + SpacetimeDB.Internal.Module.__call_procedure__( + id, + sender_0, + sender_1, + sender_2, + sender_3, + conn_id_0, + conn_id_1, + timestamp, + args, + result_sink + ); + + [UnmanagedCallersOnly(EntryPoint = "__call_view__")] + public static SpacetimeDB.Internal.Errno __call_view__( + uint id, + ulong sender_0, + ulong sender_1, + ulong sender_2, + ulong sender_3, + SpacetimeDB.Internal.BytesSource args, + SpacetimeDB.Internal.BytesSink sink + ) => + SpacetimeDB.Internal.Module.__call_view__( + id, + sender_0, + sender_1, + sender_2, + sender_3, + args, + sink + ); + + [UnmanagedCallersOnly(EntryPoint = "__call_view_anon__")] + public static SpacetimeDB.Internal.Errno __call_view_anon__( + uint id, + SpacetimeDB.Internal.BytesSource args, + SpacetimeDB.Internal.BytesSink sink + ) => SpacetimeDB.Internal.Module.__call_view_anon__(id, args, sink); +#endif +} + +#pragma warning restore STDB_UNSTABLE +#pragma warning restore CS0436 diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#Reducers.DemoProcedure.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#Reducers.DemoProcedure.verified.cs new file mode 100644 index 00000000000..ce3fd066748 --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#Reducers.DemoProcedure.verified.cs @@ -0,0 +1,18 @@ +//HintName: Reducers.DemoProcedure.cs +// +#nullable enable + +partial class Reducers +{ + [System.Diagnostics.CodeAnalysis.Experimental("STDB_UNSTABLE")] + public static void VolatileNonatomicScheduleImmediateDemoProcedure() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + + SpacetimeDB.Internal.ProcedureExtensions.VolatileNonatomicScheduleImmediate( + nameof(DemoProcedure), + stream + ); + } +} // Reducers diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#Reducers.DemoReducer.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#Reducers.DemoReducer.verified.cs new file mode 100644 index 00000000000..85af7c9566c --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Module#Reducers.DemoReducer.verified.cs @@ -0,0 +1,18 @@ +//HintName: Reducers.DemoReducer.cs +// +#nullable enable + +partial class Reducers +{ + [System.Diagnostics.CodeAnalysis.Experimental("STDB_UNSTABLE")] + public static void VolatileNonatomicScheduleImmediateDemoReducer(int value) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + new SpacetimeDB.BSATN.I32().Write(writer, value); + SpacetimeDB.Internal.IReducer.VolatileNonatomicScheduleImmediate( + nameof(DemoReducer), + stream + ); + } +} // Reducers diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Type#DemoType.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Type#DemoType.verified.cs new file mode 100644 index 00000000000..8c54a028122 --- /dev/null +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/explicitnames/snapshots/Type#DemoType.verified.cs @@ -0,0 +1,101 @@ +//HintName: DemoType.cs +// +#nullable enable + +partial struct DemoType : System.IEquatable, SpacetimeDB.BSATN.IStructuralReadWrite +{ + public void ReadFields(System.IO.BinaryReader reader) + { + A = BSATN.ARW.Read(reader); + } + + public void WriteFields(System.IO.BinaryWriter writer) + { + BSATN.ARW.Write(writer, A); + } + + object SpacetimeDB.BSATN.IStructuralReadWrite.GetSerializer() + { + return new BSATN(); + } + + public override string ToString() => + $"DemoType {{ A = {SpacetimeDB.BSATN.StringUtil.GenericToString(A)} }}"; + + public readonly partial struct BSATN : SpacetimeDB.BSATN.IReadWrite + { + internal static readonly SpacetimeDB.BSATN.I32 ARW = new(); + + public DemoType Read(System.IO.BinaryReader reader) + { + var ___result = new DemoType(); + ___result.ReadFields(reader); + return ___result; + } + + public void Write(System.IO.BinaryWriter writer, DemoType value) + { + value.WriteFields(writer); + } + + public SpacetimeDB.BSATN.AlgebraicType.Ref GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => + registrar.RegisterType(_ => new SpacetimeDB.BSATN.AlgebraicType.Product( + new SpacetimeDB.BSATN.AggregateElement[] + { + new("A", ARW.GetAlgebraicType(registrar)) + } + )); + + SpacetimeDB.BSATN.AlgebraicType SpacetimeDB.BSATN.IReadWrite.GetAlgebraicType( + SpacetimeDB.BSATN.ITypeRegistrar registrar + ) => GetAlgebraicType(registrar); + } + + public override int GetHashCode() + { + var ___hashA = A.GetHashCode(); + return ___hashA; + } + +#nullable enable + public bool Equals(DemoType that) + { + var ___eqA = this.A.Equals(that.A); + return ___eqA; + } + + public override bool Equals(object? that) + { + if (that == null) + { + return false; + } + var that_ = that as DemoType?; + if (((object?)that_) == null) + { + return false; + } + return Equals(that_); + } + + public static bool operator ==(DemoType this_, DemoType that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return object.Equals(this_, that); + } + return this_.Equals(that); + } + + public static bool operator !=(DemoType this_, DemoType that) + { + if (((object?)this_) == null || ((object?)that) == null) + { + return !object.Equals(this_, that); + } + return !this_.Equals(that); + } +#nullable restore +} // DemoType diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs index dcfcf8ba903..7db1b608cbb 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs @@ -725,8 +725,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "Location", + SourceName: "BTreeMultiColumn_X_Y_Z_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0, 1, 2]) ) ], @@ -901,18 +901,18 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "Id", + SourceName: "BTreeViews_Id_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ), new( - SourceName: null, - AccessorName: "Location", + SourceName: "BTreeViews_X_Y_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1, 2]) ), new( - SourceName: null, - AccessorName: "Faction", + SourceName: "BTreeViews_Faction_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([3]) ) ], @@ -1088,13 +1088,13 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "Foo", + SourceName: "MultiTable1_Foo_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1]) ), new( - SourceName: null, - AccessorName: "Name", + SourceName: "MultiTable1_Name_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ) ], @@ -1215,8 +1215,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "Bar", + SourceName: "MultiTable2_Bar_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([2]) ) ], @@ -1346,8 +1346,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "Id", + SourceName: "PublicTable_Id_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ) ], @@ -1429,13 +1429,13 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "Unique1", + SourceName: "RegressionMultipleUniqueIndexesHadSameName_Unique1_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ), new( - SourceName: null, - AccessorName: "Unique2", + SourceName: "RegressionMultipleUniqueIndexesHadSameName_Unique2_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([1]) ) ], @@ -1552,8 +1552,8 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [ new( - SourceName: null, - AccessorName: "ScheduledId", + SourceName: "SendMessageTimer_ScheduledId_idx_btree", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.BTree([0]) ) ], diff --git a/crates/bindings-csharp/Codegen/Diag.cs b/crates/bindings-csharp/Codegen/Diag.cs index 6b87a4028b0..00041b779f0 100644 --- a/crates/bindings-csharp/Codegen/Diag.cs +++ b/crates/bindings-csharp/Codegen/Diag.cs @@ -239,4 +239,22 @@ string typeName $"View '{method.Identifier}' must have no arguments beyond the context. This is a temporary limitation.", method => method ); + + public static readonly ErrorDescriptor SettingsMustBeConstCaseConversionPolicy = + new( + group, + "[SpacetimeDB.Settings] field must be a const CaseConversionPolicy", + field => + $"Settings field {field.Name} must be declared as 'public const SpacetimeDB.Internal.CaseConversionPolicy ...'.", + field => field + ); + + public static readonly ErrorDescriptor> DuplicateSettings = + new( + group, + "Multiple [SpacetimeDB.Settings] declarations", + fullNames => + $"[SpacetimeDB.Settings] is declared multiple times: {string.Join(", ", fullNames)}", + _ => Location.None + ); } diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index cbde9cf247b..8812ceb0b69 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -1,5 +1,6 @@ namespace SpacetimeDB.Codegen; +using System; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; @@ -55,6 +56,58 @@ attrData.AttributeClass is not { } attrClass } } +record SettingsDeclaration +{ + public readonly string FullName; + public readonly string? CaseConversionPolicy; + + private const string CaseConversionPolicyTypeName = "SpacetimeDB.Internal.CaseConversionPolicy"; + + public SettingsDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter diag) + { + var fieldSymbol = (IFieldSymbol)context.TargetSymbol; + FullName = SymbolToName(fieldSymbol); + + if (!fieldSymbol.IsConst) + { + diag.Report(ErrorDescriptor.SettingsMustBeConstCaseConversionPolicy, fieldSymbol); + return; + } + if (fieldSymbol.Type.ToString() != CaseConversionPolicyTypeName) + { + diag.Report(ErrorDescriptor.SettingsMustBeConstCaseConversionPolicy, fieldSymbol); + return; + } + if (fieldSymbol.ConstantValue is null) + { + diag.Report(ErrorDescriptor.SettingsMustBeConstCaseConversionPolicy, fieldSymbol); + return; + } + + try + { + var n = Convert.ToInt32(fieldSymbol.ConstantValue); + CaseConversionPolicy = n switch + { + 0 => "None", + 1 => "SnakeCase", + 2 => "CamelCase", + 3 => "PascalCase", + _ => null, + }; + } + catch + { + CaseConversionPolicy = null; + } + + if (CaseConversionPolicy is null) + { + diag.Report(ErrorDescriptor.SettingsMustBeConstCaseConversionPolicy, fieldSymbol); + } + } +} + /// /// Represents a reference to a column in a table, combining its index and name. /// Used to maintain references to columns for indexing and querying purposes. @@ -213,6 +266,7 @@ record Scheduled(string ReducerName, int ScheduledAtColumn); record TableAccessor { public readonly string Name; + public readonly string? CanonicalName; public readonly bool IsPublic; public readonly bool IsEvent; public readonly Scheduled? Scheduled; @@ -222,6 +276,7 @@ public TableAccessor(TableDeclaration table, AttributeData data, DiagReporter di var attr = data.ParseAs(); Name = attr.Accessor ?? table.ShortName; + CanonicalName = attr.Name; IsPublic = attr.Public; IsEvent = attr.Event; if ( @@ -272,6 +327,7 @@ record TableIndex public readonly EquatableArray Columns; public readonly string? Table; public readonly string AccessorName; + public readonly string? CanonicalName; public readonly TableIndexType Type; // See: bindings_sys::index_id_from_name for documentation of this format. @@ -283,11 +339,13 @@ record TableIndex /// Other constructors delegate to this one to avoid code duplication. /// /// Name to use when accessing this index. If null, will be generated from column names. + /// Explicit canonical name override for this index, if any. /// The columns that make up this index. /// The name of the table this index belongs to, if any. /// The type of index (currently only B-tree is supported). private TableIndex( string? accessorName, + string? canonicalName, ImmutableArray columns, string? tableName, TableIndexType type @@ -297,6 +355,7 @@ TableIndexType type Table = tableName; var columnNames = string.Join("_", columns.Select(c => c.Name)); AccessorName = accessorName ?? columnNames; + CanonicalName = canonicalName; Type = type; StandardNameSuffix = $"_{columnNames}_idx_{Type.ToString().ToLower()}"; } @@ -307,6 +366,7 @@ TableIndexType type /// The column to index. public TableIndex(ColumnRef col) : this( + null, null, ImmutableArray.Create(col), null, @@ -317,8 +377,11 @@ public TableIndex(ColumnRef col) /// Creates an index with the given attribute and columns. /// Used internally by other constructors that parse attributes. /// - private TableIndex(Index.BTreeAttribute attr, ImmutableArray columns) - : this(attr.Accessor, columns, attr.Table, TableIndexType.BTree) { } + private TableIndex( + global::SpacetimeDB.Index.BTreeAttribute attr, + ImmutableArray columns + ) + : this(attr.Accessor, attr.Name, columns, attr.Table, TableIndexType.BTree) { } /// /// Creates an index from a table declaration and attribute data. @@ -326,7 +389,7 @@ private TableIndex(Index.BTreeAttribute attr, ImmutableArray columns) /// private TableIndex( TableDeclaration table, - Index.BTreeAttribute attr, + global::SpacetimeDB.Index.BTreeAttribute attr, AttributeData data, DiagReporter diag ) @@ -350,7 +413,7 @@ DiagReporter diag /// Creates an index by parsing attribute data from a table declaration. /// public TableIndex(TableDeclaration table, AttributeData data, DiagReporter diag) - : this(table, data.ParseAs(), data, diag) { } + : this(table, data.ParseAs(), data, diag) { } /// /// Creates an index for a single column with attribute data. @@ -358,7 +421,7 @@ public TableIndex(TableDeclaration table, AttributeData data, DiagReporter diag) /// private TableIndex( ColumnRef column, - Index.BTreeAttribute attr, + global::SpacetimeDB.Index.BTreeAttribute attr, AttributeData data, DiagReporter diag ) @@ -374,24 +437,22 @@ DiagReporter diag /// Creates an index for a single column by parsing attribute data. /// public TableIndex(ColumnRef col, AttributeData data, DiagReporter diag) - : this(col, data.ParseAs(), data, diag) { } + : this(col, data.ParseAs(), data, diag) { } // `FullName` and Roslyn have different ways of representing nested types in full names - // one uses a `Parent+Child` syntax, the other uses `Parent.Child`. // Manually fixup one to the other. - private static readonly string BTreeAttrName = typeof(Index.BTreeAttribute).FullName.Replace( - '+', - '.' - ); + private static readonly string BTreeAttrName = + typeof(global::SpacetimeDB.Index.BTreeAttribute).FullName.Replace('+', '.'); public static bool CanParse(AttributeData data) => data.AttributeClass?.ToString() == BTreeAttrName; - public string GenerateIndexDef() => + public string GenerateIndexDef(TableAccessor tableAccessor) => $$""" new( - SourceName: null, - AccessorName: "{{AccessorName}}", + SourceName: "{{StandardIndexName(tableAccessor)}}", + AccessorName: null, Algorithm: new SpacetimeDB.Internal.RawIndexAlgorithm.{{Type}}([{{string.Join( ", ", Columns.Select(c => c.Index) @@ -744,7 +805,7 @@ public IEnumerable GenerateTableAccessors() GetConstraints(v, ColumnAttrs.Unique) .Select(c => c.ToIndex()) .Concat(GetIndexes(v)) - .Select(b => b.GenerateIndexDef()) + .Select(b => b.GenerateIndexDef(v)) )}}} ], Constraints: {{{GenConstraintList(v, ColumnAttrs.Unique, $"{iTable}.MakeUniqueConstraint")}}}, @@ -1050,6 +1111,7 @@ string makeConstraintFn record ViewDeclaration { public readonly string Name; + public readonly string? CanonicalName; public readonly string FullName; public readonly bool IsAnonymous; public readonly bool IsPublic; @@ -1083,6 +1145,7 @@ public ViewDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter dia } Name = attr.Accessor ?? method.Name; + CanonicalName = attr.Name; FullName = SymbolToName(method); IsPublic = attr.Public; IsAnonymous = isAnonymousContext; @@ -1250,6 +1313,7 @@ public byte[] Invoke( record ReducerDeclaration { public readonly string Name; + public readonly string? CanonicalName; public readonly ReducerKind Kind; public readonly string FullName; public readonly EquatableArray Args; @@ -1287,6 +1351,7 @@ public ReducerDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter } Kind = attr.Kind; + CanonicalName = attr.Name; FullName = SymbolToName(method); Args = new( method @@ -1369,14 +1434,15 @@ public Scope.Extensions GenerateSchedule() record ProcedureDeclaration { public readonly string Name; + public readonly string? CanonicalName; public readonly string FullName; public readonly EquatableArray Args; public readonly Scope Scope; private readonly bool HasWrongSignature; public readonly TypeUse ReturnType; - private readonly IMethodSymbol _methodSymbol; - private readonly ITypeSymbol _returnTypeSymbol; - private readonly DiagReporter _diag; + private readonly bool HasTxWrapper; + private readonly TypeUse? TxPayloadType; + private readonly bool TxPayloadIsUnit; public ProcedureDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter diag) { @@ -1384,10 +1450,6 @@ public ProcedureDeclaration(GeneratorAttributeSyntaxContext context, DiagReporte var method = (IMethodSymbol)context.TargetSymbol; var attr = context.Attributes.Single().ParseAs(); - _methodSymbol = method; - _returnTypeSymbol = method.ReturnType; - _diag = diag; - if ( method.Parameters.FirstOrDefault()?.Type is not INamedTypeSymbol { Name: "ProcedureContext" } @@ -1409,6 +1471,37 @@ public ProcedureDeclaration(GeneratorAttributeSyntaxContext context, DiagReporte ReturnType = TypeUse.Parse(method, method.ReturnType, diag); + if ( + method.ReturnType + is INamedTypeSymbol + { + Name: "TxOutcome", + ContainingType: { Name: "ProcedureContext" } + } txOutcome + && txOutcome.TypeArguments.Length == 1 + ) + { + HasTxWrapper = true; + TxPayloadType = TypeUse.Parse(method, txOutcome.TypeArguments[0], diag); + TxPayloadIsUnit = TxPayloadType.BSATNName == "SpacetimeDB.BSATN.Unit"; + } + else if ( + method.ReturnType + is INamedTypeSymbol + { + Name: "TxResult", + ContainingType: { Name: "ProcedureContext" } + } txResult + && txResult.TypeArguments.Length == 2 + ) + { + HasTxWrapper = true; + TxPayloadType = TypeUse.Parse(method, txResult.TypeArguments[0], diag); + TxPayloadIsUnit = TxPayloadType.BSATNName == "SpacetimeDB.BSATN.Unit"; + } + + CanonicalName = attr.Name; + FullName = SymbolToName(method); Args = new( method @@ -1425,11 +1518,8 @@ public string GenerateClass() Args.Length == 0 ? "" : ", " + string.Join(", ", Args.Select(a => a.Name)); var invocation = $"{FullName}((SpacetimeDB.ProcedureContext)ctx{invocationArgs})"; - var hasTxOutcome = TryGetTxOutcomeType(out var txOutcomePayload); - var hasTxResult = TryGetTxResultTypes(out var txResultPayload, out _); - var hasTxWrapper = hasTxOutcome || hasTxResult; - var txPayload = hasTxOutcome ? txOutcomePayload : txResultPayload; - var txPayloadIsUnit = hasTxWrapper && txPayload.BSATNName == "SpacetimeDB.BSATN.Unit"; + var txPayload = TxPayloadType ?? ReturnType; + var txPayloadIsUnit = TxPayloadIsUnit; string[] bodyLines; @@ -1440,7 +1530,7 @@ public string GenerateClass() "throw new System.InvalidOperationException(\"Invalid procedure signature.\");", }; } - else if (hasTxWrapper) + else if (HasTxWrapper) { var successLines = txPayloadIsUnit ? new[] { "return System.Array.Empty();" } @@ -1491,7 +1581,7 @@ public string GenerateClass() ) ) + "\n"; - var returnTypeExpr = hasTxWrapper + var returnTypeExpr = HasTxWrapper ? ( txPayloadIsUnit ? "SpacetimeDB.BSATN.AlgebraicType.Unit" @@ -1504,7 +1594,7 @@ public string GenerateClass() ); var classFields = MemberDeclaration.GenerateBsatnFields(Accessibility.Private, Args); - if (hasTxWrapper && !txPayloadIsUnit) + if (HasTxWrapper && !txPayloadIsUnit) { classFields += $"\n private {txPayload.BSATNName} __txReturnRW = new {txPayload.BSATNName}();"; @@ -1556,48 +1646,6 @@ public Scope.Extensions GenerateSchedule() return extensions; } - - private bool TryGetTxOutcomeType(out TypeUse payloadType) - { - if ( - _returnTypeSymbol - is INamedTypeSymbol - { - Name: "TxOutcome", - ContainingType: { Name: "ProcedureContext" } - } named - && named.TypeArguments.Length == 1 - ) - { - payloadType = TypeUse.Parse(_methodSymbol, named.TypeArguments[0], _diag); - return true; - } - - payloadType = default!; - return false; - } - - private bool TryGetTxResultTypes(out TypeUse payloadType, out TypeUse errorType) - { - if ( - _returnTypeSymbol - is INamedTypeSymbol - { - Name: "TxResult", - ContainingType: { Name: "ProcedureContext" } - } named - && named.TypeArguments.Length == 2 - ) - { - payloadType = TypeUse.Parse(_methodSymbol, named.TypeArguments[0], _diag); - errorType = TypeUse.Parse(_methodSymbol, named.TypeArguments[1], _diag); - return true; - } - - payloadType = default!; - errorType = default!; - return false; - } } record ClientVisibilityFilterDeclaration @@ -1634,6 +1682,13 @@ DiagReporter diag [Generator] public class Module : IIncrementalGenerator { + private static string EscapeStringLiteral(string s) => + s.Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r", "\\r") + .Replace("\n", "\\n") + .Replace("\t", "\\t"); + /// /// Collects distinct items from a source sequence, ensuring no duplicate export names exist. /// @@ -1704,6 +1759,24 @@ Func toFullName public void Initialize(IncrementalGeneratorInitializationContext context) { + var settings = context + .SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName: typeof(SettingsAttribute).FullName, + predicate: (node, ct) => true, + transform: (context, ct) => + context.ParseWithDiags(diag => new SettingsDeclaration(context, diag)) + ) + .ReportDiagnostics(context) + .WithTrackingName("SpacetimeDB.Settings.Parse"); + + var settingsArray = CollectDistinct( + "Settings", + context, + settings, + s => s.FullName, + s => s.FullName + ); + var tables = context .SyntaxProvider.ForAttributeWithMetadataName( fullyQualifiedMetadataName: typeof(TableAttribute).FullName, @@ -1781,7 +1854,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) "Reducer", context, reducers - .Select((r, ct) => (r.Name, r.FullName, Class: r.GenerateClass())) + .Select((r, ct) => (r.Name, r.FullName, r.CanonicalName, Class: r.GenerateClass())) .WithTrackingName("SpacetimeDB.Reducer.GenerateClass"), r => r.Name, r => r.FullName @@ -1806,7 +1879,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) "Procedure", context, procedures - .Select((p, ct) => (p.Name, p.FullName, Class: p.GenerateClass())) + .Select((p, ct) => (p.Name, p.FullName, p.CanonicalName, Class: p.GenerateClass())) .WithTrackingName("SpacetimeDB.Procedure.GenerateClass"), p => p.Name, p => p.FullName @@ -1869,6 +1942,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Once the compilation is complete, the generated code will be used to create tables and reducers in the database context.RegisterSourceOutput( tableAccessors + .Combine(settingsArray) .Combine(tableDecls) .Combine(addReducers) .Combine(addProcedures) @@ -1882,7 +1956,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ( ( ( - (((tableAccessors, tableDecls), addReducers), addProcedures), + ( + (((tableAccessors, settings), tableDecls), addReducers), + addProcedures + ), readOnlyAccessors ), views @@ -1892,6 +1969,84 @@ public void Initialize(IncrementalGeneratorInitializationContext context) columnDefaultValues ) = tuple; + if (settings.Array.Length > 1) + { + context.ReportDiagnostic( + ErrorDescriptor.DuplicateSettings.ToDiag( + settings.Array.Select(s => s.FullName) + ) + ); + } + + var settingsRegistration = + settings.Array.Length == 1 + && settings.Array[0].CaseConversionPolicy is { } policyName + ? $"SpacetimeDB.Internal.Module.SetCaseConversionPolicy(SpacetimeDB.Internal.CaseConversionPolicy.{policyName});" + : string.Empty; + + var explicitTableRegistrations = string.Join( + "\n", + tableDecls.Array.SelectMany(t => + t.TableAccessors.Where(a => !string.IsNullOrEmpty(a.CanonicalName)) + .Select(a => + $"SpacetimeDB.Internal.Module.RegisterExplicitTableName(\"{EscapeStringLiteral(a.Name)}\", \"{EscapeStringLiteral(a.CanonicalName!)}\");" + ) + ) + ); + + var explicitFunctionRegistrations = string.Join( + "\n", + addReducers + .Array.Where(r => !string.IsNullOrEmpty(r.CanonicalName)) + .Select(r => + $"SpacetimeDB.Internal.Module.RegisterExplicitFunctionName(\"{EscapeStringLiteral(r.Name)}\", \"{EscapeStringLiteral(r.CanonicalName!)}\");" + ) + .Concat( + addProcedures + .Array.Where(p => !string.IsNullOrEmpty(p.CanonicalName)) + .Select(p => + $"SpacetimeDB.Internal.Module.RegisterExplicitFunctionName(\"{EscapeStringLiteral(p.Name)}\", \"{EscapeStringLiteral(p.CanonicalName!)}\");" + ) + ) + .Concat( + views + .Array.Where(v => !string.IsNullOrEmpty(v.CanonicalName)) + .Select(v => + $"SpacetimeDB.Internal.Module.RegisterExplicitFunctionName(\"{EscapeStringLiteral(v.Name)}\", \"{EscapeStringLiteral(v.CanonicalName!)}\");" + ) + ) + ); + + var explicitIndexRegistrations = string.Join( + "\n", + tableDecls.Array.SelectMany(t => + t.TableAccessors.SelectMany(a => + t.GetIndexes(a) + .Where(ix => !string.IsNullOrEmpty(ix.CanonicalName)) + .Select(ix => + $"SpacetimeDB.Internal.Module.RegisterExplicitIndexName(\"{EscapeStringLiteral(ix.StandardIndexName(a))}\", \"{EscapeStringLiteral(ix.CanonicalName!)}\");" + ) + ) + ) + ); + + var preRegistrationLines = new[] + { + settingsRegistration, + explicitTableRegistrations, + explicitFunctionRegistrations, + explicitIndexRegistrations, + } + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToArray(); + + var preRegistrations = + preRegistrationLines.Length == 0 + ? string.Empty + : "\n " + + string.Join("\n ", preRegistrationLines) + + "\n"; + var queryBuilderMembers = string.Join( "\n", tableDecls.Array.SelectMany(t => t.GenerateQueryBuilderMembers()) @@ -2145,7 +2300,7 @@ public static void Main() { SpacetimeDB.Internal.Module.SetReducerContextConstructor((identity, connectionId, random, time) => new SpacetimeDB.ReducerContext(identity, connectionId, random, time)); SpacetimeDB.Internal.Module.SetViewContextConstructor(identity => new SpacetimeDB.ViewContext(identity, new SpacetimeDB.Internal.LocalReadOnly())); SpacetimeDB.Internal.Module.SetAnonymousViewContextConstructor(() => new SpacetimeDB.AnonymousViewContext(new SpacetimeDB.Internal.LocalReadOnly())); - SpacetimeDB.Internal.Module.SetProcedureContextConstructor((identity, connectionId, random, time) => new SpacetimeDB.ProcedureContext(identity, connectionId, random, time)); + SpacetimeDB.Internal.Module.SetProcedureContextConstructor((identity, connectionId, random, time) => new SpacetimeDB.ProcedureContext(identity, connectionId, random, time));{{preRegistrations}} var __memoryStream = new MemoryStream(); var __writer = new BinaryWriter(__memoryStream); diff --git a/crates/bindings-csharp/Runtime/Attrs.cs b/crates/bindings-csharp/Runtime/Attrs.cs index f9f46e5090a..67fceffebaf 100644 --- a/crates/bindings-csharp/Runtime/Attrs.cs +++ b/crates/bindings-csharp/Runtime/Attrs.cs @@ -41,6 +41,9 @@ public abstract class ColumnAttribute : Attribute [AttributeUsage(AttributeTargets.Field)] public sealed class ClientVisibilityFilterAttribute : Attribute { } + [AttributeUsage(AttributeTargets.Field)] + public sealed class SettingsAttribute : Attribute { } + /// /// Registers a type as the row structure of a SpacetimeDB table, enabling codegen for it. /// @@ -60,6 +63,8 @@ public sealed class TableAttribute : Attribute /// public string? Accessor { get; init; } + public string? Name { get; init; } + /// /// Set to true to make the table visible to everyone. /// @@ -94,6 +99,8 @@ public sealed class ViewAttribute : Attribute /// public string? Accessor { get; init; } + public string? Name { get; init; } + /// /// Marks the view as callable by any client. Leave false to restrict to the module owner. /// @@ -110,6 +117,8 @@ public abstract class Index : Attribute public string? Accessor { get; init; } + public string? Name { get; init; } + public sealed class BTreeAttribute : Index { public string[] Columns { get; init; } = []; @@ -185,8 +194,13 @@ public enum ReducerKind public sealed class ReducerAttribute(ReducerKind kind = ReducerKind.UserDefined) : Attribute { public ReducerKind Kind => kind; + + public string? Name { get; init; } } [AttributeUsage(AttributeTargets.Method, Inherited = false)] - public sealed class ProcedureAttribute() : Attribute { } + public sealed class ProcedureAttribute() : Attribute + { + public string? Name { get; init; } + } } diff --git a/crates/bindings-csharp/Runtime/Internal/Module.cs b/crates/bindings-csharp/Runtime/Internal/Module.cs index 1809c4a73af..468162dd5f6 100644 --- a/crates/bindings-csharp/Runtime/Internal/Module.cs +++ b/crates/bindings-csharp/Runtime/Internal/Module.cs @@ -21,6 +21,9 @@ partial class RawModuleDefV10 private readonly Dictionary> defaultValuesByTable = new(StringComparer.Ordinal); + private CaseConversionPolicy? caseConversionPolicy = null; + private readonly List explicitNames = []; + // Note: this intends to generate a valid identifier, but it's not guaranteed to be unique as it's not proper mangling. // Fix it up to a different mangling scheme if it causes problems. private static string GetFriendlyName(Type type) => @@ -86,6 +89,20 @@ internal void RegisterTableDefaultValue(string table, ushort colId, byte[] value defaults.Add(new RawColumnDefaultValueV10(colId, new List(value))); } + internal void SetCaseConversionPolicy(CaseConversionPolicy policy) => + caseConversionPolicy = policy; + + internal void RegisterExplicitTableName(string sourceName, string canonicalName) => + explicitNames.Add(new ExplicitNameEntry.Table(new NameMapping(sourceName, canonicalName))); + + internal void RegisterExplicitFunctionName(string sourceName, string canonicalName) => + explicitNames.Add( + new ExplicitNameEntry.Function(new NameMapping(sourceName, canonicalName)) + ); + + internal void RegisterExplicitIndexName(string sourceName, string canonicalName) => + explicitNames.Add(new ExplicitNameEntry.Index(new NameMapping(sourceName, canonicalName))); + internal RawModuleDefV10 BuildModuleDefinition() { var builtTables = new List(tableDefs.Count); @@ -165,6 +182,18 @@ internal RawModuleDefV10 BuildModuleDefinition() sections.Add(new RawModuleDefV10Section.LifeCycleReducers(lifecycleReducerDefs)); } // TODO: Add sections for Event tables and Case conversion policy (mirrors Rust `raw_def/v10.rs` TODO). + if (caseConversionPolicy is { } policy) + { + sections.Add(new RawModuleDefV10Section.CaseConversionPolicy(policy)); + } + if (explicitNames.Count > 0) + { + sections.Add( + new RawModuleDefV10Section.ExplicitNames( + new ExplicitNames(new List(explicitNames)) + ) + ); + } if (rowLevelSecurityDefs.Count > 0) { sections.Add(new RawModuleDefV10Section.RowLevelSecurity(rowLevelSecurityDefs)); @@ -303,6 +332,18 @@ public static void RegisterClientVisibilityFilter(Filter rlsFilter) public static void RegisterTableDefaultValue(string table, ushort colId, byte[] value) => moduleDef.RegisterTableDefaultValue(table, colId, value); + public static void SetCaseConversionPolicy(CaseConversionPolicy policy) => + moduleDef.SetCaseConversionPolicy(policy); + + public static void RegisterExplicitTableName(string sourceName, string canonicalName) => + moduleDef.RegisterExplicitTableName(sourceName, canonicalName); + + public static void RegisterExplicitFunctionName(string sourceName, string canonicalName) => + moduleDef.RegisterExplicitFunctionName(sourceName, canonicalName); + + public static void RegisterExplicitIndexName(string sourceName, string canonicalName) => + moduleDef.RegisterExplicitIndexName(sourceName, canonicalName); + public static byte[] Consume(this BytesSource source) { if (source == BytesSource.INVALID)