From 46ff4328208d9737f7de8b459d29bc5e3f252ace Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 6 Jun 2025 09:35:24 -0400 Subject: [PATCH 1/4] first iteration of listener attribute and global simulator --- core/Attributes/ListenerAttribute.cs | 12 ++ core/StaticSimulator.cs | 82 ++++++++++ generator/Constants.cs | 8 +- .../GlobalSimulatorLoaderGenerator.cs | 144 ++++++++++++++++++ .../Generators/ListenerCollectorGenerator.cs | 7 +- generator/ListenerMethod.cs | 19 +++ generator/SystemType.cs | 2 +- tests/BroadcastingTests.cs | 12 ++ tests/Systems/GlobalTimeSystem.cs | 13 ++ 9 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 core/Attributes/ListenerAttribute.cs create mode 100644 core/StaticSimulator.cs create mode 100644 generator/Generators/GlobalSimulatorLoaderGenerator.cs create mode 100644 generator/ListenerMethod.cs create mode 100644 tests/Systems/GlobalTimeSystem.cs diff --git a/core/Attributes/ListenerAttribute.cs b/core/Attributes/ListenerAttribute.cs new file mode 100644 index 0000000..cd0a753 --- /dev/null +++ b/core/Attributes/ListenerAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Simulation +{ + /// + /// Attribute to mark static methods as listeners for messages of type . + /// + [AttributeUsage(AttributeTargets.Method)] + public class ListenerAttribute : Attribute where T : unmanaged + { + } +} diff --git a/core/StaticSimulator.cs b/core/StaticSimulator.cs new file mode 100644 index 0000000..7ef9582 --- /dev/null +++ b/core/StaticSimulator.cs @@ -0,0 +1,82 @@ +using System.Collections; +using System.Collections.Generic; +using Types; + +namespace Simulation +{ + /// + /// Dispatches messages for static methods with the . + /// + public static class GlobalSimulator + { + private static readonly Dictionary listeners = new(); + + /// + /// Registers a listener for messages of type . + /// + public static void Register(Receive receive) where T : unmanaged + { + TypeMetadata messageType = TypeMetadata.GetOrRegister(); + Listeners.list.Add(receive); + listeners[messageType] = Listeners.list; + } + + /// + /// Registers a . + /// + public static void Register(IListener listener) where T : unmanaged + { + TypeMetadata messageType = TypeMetadata.GetOrRegister(); + Receive receive = listener.Receive; + Listeners.list.Add(receive); + listeners[messageType] = Listeners.list; + } + + /// + /// Resets the global simulator to initial state, with no listeners registered. + /// + public static void Reset() + { + foreach (IList list in listeners.Values) + { + list.Clear(); + } + + listeners.Clear(); + } + + /// + /// Broadcasts the given . + /// + public static void Broadcast(T message) where T : unmanaged + { + int length = Listeners.list.Count; + for (int i = 0; i < length; i++) + { + Listeners.list[i](ref message); + } + } + + /// + /// Broadcasts the given . + /// + public static void Broadcast(ref T message) where T : unmanaged + { + int length = Listeners.list.Count; + for (int i = 0; i < length; i++) + { + Listeners.list[i](ref message); + } + } + + private static class Listeners where T : unmanaged + { + public static readonly List> list = new(); + } + + /// + /// Delegate for message receivers. + /// + public delegate void Receive(ref T message) where T : unmanaged; + } +} \ No newline at end of file diff --git a/generator/Constants.cs b/generator/Constants.cs index 83b1b48..1742604 100644 --- a/generator/Constants.cs +++ b/generator/Constants.cs @@ -1,7 +1,4 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Simulation.Generator.Tests")] -namespace Simulation +namespace Simulation { internal static class Constants { @@ -10,5 +7,8 @@ internal static class Constants public const string ListenerInterfaceTypeName = Namespace + ".IListener"; public const string SystemBankTypeNameFormat = "{0}SystemBank"; public const string PluralSystemBankTypeNameFormat = "{0}SystemsBank"; + public const string GlobalSimulatorTypeName = "GlobalSimulator"; + public const string GlobalSimulatorLoaderTypeName = GlobalSimulatorTypeName + "Loader"; + public const string ListenerAttributeTypeName = "ListenerAttribute"; } } \ No newline at end of file diff --git a/generator/Generators/GlobalSimulatorLoaderGenerator.cs b/generator/Generators/GlobalSimulatorLoaderGenerator.cs new file mode 100644 index 0000000..06b30fb --- /dev/null +++ b/generator/Generators/GlobalSimulatorLoaderGenerator.cs @@ -0,0 +1,144 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using static Simulation.Constants; + +namespace Simulation.Generators +{ + [Generator(LanguageNames.CSharp)] + internal class GlobalSimulatorLoaderGenerator : IIncrementalGenerator + { + private static readonly SourceBuilder source = new(); + + void IIncrementalGenerator.Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider filter = context.SyntaxProvider.CreateSyntaxProvider(Predicate, Transform); + context.RegisterSourceOutput(filter.Collect().Combine(context.CompilationProvider), Generate); + } + + private static bool Predicate(SyntaxNode node, CancellationToken token) + { + return node.IsKind(SyntaxKind.MethodDeclaration); + } + + private static ListenerMethod? Transform(GeneratorSyntaxContext context, CancellationToken token) + { + if (context.Node is MethodDeclarationSyntax methodDeclaration) + { + //check if the method has an attribute + SyntaxList attributes = methodDeclaration.AttributeLists; + if (attributes.Count == 0) + { + return null; + } + + const string ListenerNamePrefix = Namespace + "." + ListenerAttributeTypeName + "<"; + foreach (AttributeListSyntax attributeList in attributes) + { + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + if (context.SemanticModel.GetSymbolInfo(attribute, token).Symbol is IMethodSymbol attributeConstructorSymbol) + { + INamedTypeSymbol attributeSymbol = attributeConstructorSymbol.ContainingType; + if (attributeSymbol.TypeArguments.Length == 1) + { + ITypeSymbol messageTypeSymbol = attributeSymbol.TypeArguments[0]; + if (attributeSymbol.ToDisplayString().StartsWith(ListenerNamePrefix)) + { + if (context.SemanticModel.GetDeclaredSymbol(methodDeclaration, token) is IMethodSymbol methodSymbol) + { + string declaringTypeName = methodSymbol.ContainingType.ToDisplayString(); + return new ListenerMethod(declaringTypeName, methodDeclaration, messageTypeSymbol); + } + } + } + } + } + } + } + + return null; + } + + private void Generate(SourceProductionContext context, (ImmutableArray methods, Compilation compilation) input) + { + if (input.compilation.GetEntryPoint(context.CancellationToken) is not null) + { + List methods = new(); + foreach (ListenerMethod? method in input.methods) + { + if (method is not null) + { + methods.Add(method); + } + } + + if (methods.Count > 0) + { + context.AddSource($"{GlobalSimulatorLoaderTypeName}.generated.cs", Generate(input.compilation, methods)); + } + else + { + context.AddSource($"{GlobalSimulatorLoaderTypeName}.generated.cs", $"//{methods.Count}"); + } + } + } + + public static string Generate(Compilation compilation, IEnumerable methods) + { + string? assemblyName = compilation.AssemblyName; + source.Clear(); + source.AppendLine("using System;"); + source.AppendLine("using Simulation;"); + source.AppendLine(); + + if (assemblyName is not null) + { + source.Append("namespace "); + source.Append(assemblyName); + source.AppendLine(); + source.BeginGroup(); + } + + source.Append("public static class "); + source.Append(GlobalSimulatorLoaderTypeName); + source.AppendLine(); + + source.BeginGroup(); + { + source.AppendLine("public static void Load()"); + source.BeginGroup(); + { + source.Append(GlobalSimulatorTypeName); + source.Append(".Reset();"); + source.AppendLine(); + + foreach (ListenerMethod method in methods) + { + source.Append(GlobalSimulatorTypeName); + source.Append(".Register<"); + source.Append(method.messageTypeSymbol.ToDisplayString()); + source.Append(">("); + source.Append(method.declaringTypeName); + source.Append("."); + source.Append(method.methodDeclaration.Identifier.Text); + source.Append(");"); + source.AppendLine(); + } + } + source.EndGroup(); + } + source.EndGroup(); + + if (assemblyName is not null) + { + source.EndGroup(); + } + + return source.ToString(); + } + } +} \ No newline at end of file diff --git a/generator/Generators/ListenerCollectorGenerator.cs b/generator/Generators/ListenerCollectorGenerator.cs index 338742d..f5127a3 100644 --- a/generator/Generators/ListenerCollectorGenerator.cs +++ b/generator/Generators/ListenerCollectorGenerator.cs @@ -80,11 +80,14 @@ public static string Generate(SystemType input) if (input.containingNamespace is not null) { - source.AppendLine($"namespace {input.containingNamespace}"); + source.Append("namespace "); + source.Append(input.containingNamespace); + source.AppendLine(); source.BeginGroup(); } - source.Append($"public partial class {input.typeName}"); + source.Append("public partial class "); + source.Append(input.typeName); source.Append(" : "); source.Append(ListenerInterfaceTypeName); source.AppendLine(); diff --git a/generator/ListenerMethod.cs b/generator/ListenerMethod.cs new file mode 100644 index 0000000..7c7912c --- /dev/null +++ b/generator/ListenerMethod.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Simulation +{ + internal class ListenerMethod + { + public readonly string declaringTypeName; + public readonly MethodDeclarationSyntax methodDeclaration; + public readonly ITypeSymbol messageTypeSymbol; + + public ListenerMethod(string declaringTypeName, MethodDeclarationSyntax methodDeclaration, ITypeSymbol messageTypeSymbol) + { + this.declaringTypeName = declaringTypeName; + this.methodDeclaration = methodDeclaration; + this.messageTypeSymbol = messageTypeSymbol; + } + } +} \ No newline at end of file diff --git a/generator/SystemType.cs b/generator/SystemType.cs index 5ffd06e..54b8ad7 100644 --- a/generator/SystemType.cs +++ b/generator/SystemType.cs @@ -4,7 +4,7 @@ namespace Simulation { - public class SystemType + internal class SystemType { public readonly TypeDeclarationSyntax typeDeclaration; public readonly ITypeSymbol typeSymbol; diff --git a/tests/BroadcastingTests.cs b/tests/BroadcastingTests.cs index af20c22..f3428f6 100644 --- a/tests/BroadcastingTests.cs +++ b/tests/BroadcastingTests.cs @@ -18,6 +18,18 @@ public void BroadcastingMessages() Assert.That(system.time, Is.EqualTo(2)); } + [Test] + public void BroadcastingToGlobalSimulator() + { + GlobalSimulatorLoader.Load(); + Assert.That(GlobalTimeSystem.time, Is.EqualTo(0)); + GlobalSimulator.Broadcast(new UpdateMessage(1)); + Assert.That(GlobalTimeSystem.time, Is.EqualTo(1)); + GlobalSimulator.Reset(); + GlobalSimulator.Broadcast(new UpdateMessage(1)); + Assert.That(GlobalTimeSystem.time, Is.EqualTo(1)); + } + [Test] public void SystemsOutOfOrder() { diff --git a/tests/Systems/GlobalTimeSystem.cs b/tests/Systems/GlobalTimeSystem.cs new file mode 100644 index 0000000..d96482a --- /dev/null +++ b/tests/Systems/GlobalTimeSystem.cs @@ -0,0 +1,13 @@ +namespace Simulation.Tests +{ + public static class GlobalTimeSystem + { + public static double time; + + [Listener] + public static void OnUpdate(ref UpdateMessage message) + { + time += message.deltaTime; + } + } +} \ No newline at end of file From 46e89e9b5144eb9a67e46351674edcc8dff3bb33 Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 6 Jun 2025 09:45:48 -0400 Subject: [PATCH 2/4] optimize the global simulator --- ...{StaticSimulator.cs => GlobalSimulator.cs} | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) rename core/{StaticSimulator.cs => GlobalSimulator.cs} (69%) diff --git a/core/StaticSimulator.cs b/core/GlobalSimulator.cs similarity index 69% rename from core/StaticSimulator.cs rename to core/GlobalSimulator.cs index 7ef9582..6ceb714 100644 --- a/core/StaticSimulator.cs +++ b/core/GlobalSimulator.cs @@ -1,6 +1,5 @@ -using System.Collections; +using System; using System.Collections.Generic; -using Types; namespace Simulation { @@ -9,16 +8,15 @@ namespace Simulation /// public static class GlobalSimulator { - private static readonly Dictionary listeners = new(); + private static readonly List clearFunctions = new(); /// /// Registers a listener for messages of type . /// public static void Register(Receive receive) where T : unmanaged { - TypeMetadata messageType = TypeMetadata.GetOrRegister(); - Listeners.list.Add(receive); - listeners[messageType] = Listeners.list; + Array.Resize(ref Listeners.list, Listeners.list.Length + 1); + Listeners.list[^1] = receive; } /// @@ -26,10 +24,9 @@ public static void Register(Receive receive) where T : unmanaged /// public static void Register(IListener listener) where T : unmanaged { - TypeMetadata messageType = TypeMetadata.GetOrRegister(); Receive receive = listener.Receive; - Listeners.list.Add(receive); - listeners[messageType] = Listeners.list; + Array.Resize(ref Listeners.list, Listeners.list.Length + 1); + Listeners.list[^1] = receive; } /// @@ -37,12 +34,10 @@ public static void Register(IListener listener) where T : unmanaged /// public static void Reset() { - foreach (IList list in listeners.Values) + foreach (Action clearFunction in clearFunctions) { - list.Clear(); + clearFunction(); } - - listeners.Clear(); } /// @@ -50,7 +45,7 @@ public static void Reset() /// public static void Broadcast(T message) where T : unmanaged { - int length = Listeners.list.Count; + int length = Listeners.list.Length; for (int i = 0; i < length; i++) { Listeners.list[i](ref message); @@ -62,7 +57,7 @@ public static void Broadcast(T message) where T : unmanaged /// public static void Broadcast(ref T message) where T : unmanaged { - int length = Listeners.list.Count; + int length = Listeners.list.Length; for (int i = 0; i < length; i++) { Listeners.list[i](ref message); @@ -71,7 +66,17 @@ public static void Broadcast(ref T message) where T : unmanaged private static class Listeners where T : unmanaged { - public static readonly List> list = new(); + public static Receive[] list = []; + + static Listeners() + { + clearFunctions.Add(Reset); + } + + public static void Reset() + { + Array.Resize(ref list, 0); + } } /// From d883acef171f172a47184d970b331929a01066b2 Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 6 Jun 2025 11:31:31 -0400 Subject: [PATCH 3/4] emit errors when a listener method signature isnt valid --- .../GlobalSimulatorLoaderGenerator.cs | 90 +++++++++++++++++-- generator/ListenerMethod.cs | 6 +- 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/generator/Generators/GlobalSimulatorLoaderGenerator.cs b/generator/Generators/GlobalSimulatorLoaderGenerator.cs index 06b30fb..218a239 100644 --- a/generator/Generators/GlobalSimulatorLoaderGenerator.cs +++ b/generator/Generators/GlobalSimulatorLoaderGenerator.cs @@ -50,8 +50,7 @@ private static bool Predicate(SyntaxNode node, CancellationToken token) { if (context.SemanticModel.GetDeclaredSymbol(methodDeclaration, token) is IMethodSymbol methodSymbol) { - string declaringTypeName = methodSymbol.ContainingType.ToDisplayString(); - return new ListenerMethod(declaringTypeName, methodDeclaration, messageTypeSymbol); + return new ListenerMethod(methodSymbol, methodDeclaration, messageTypeSymbol); } } } @@ -78,16 +77,12 @@ private void Generate(SourceProductionContext context, (ImmutableArray 0) { - context.AddSource($"{GlobalSimulatorLoaderTypeName}.generated.cs", Generate(input.compilation, methods)); - } - else - { - context.AddSource($"{GlobalSimulatorLoaderTypeName}.generated.cs", $"//{methods.Count}"); + context.AddSource($"{GlobalSimulatorLoaderTypeName}.generated.cs", Generate(context, input.compilation, methods)); } } } - public static string Generate(Compilation compilation, IEnumerable methods) + public static string Generate(SourceProductionContext context, Compilation compilation, IEnumerable methods) { string? assemblyName = compilation.AssemblyName; source.Clear(); @@ -118,11 +113,88 @@ public static string Generate(Compilation compilation, IEnumerable 1) + { + const string ID = "S0005"; + const string Category = "Listeners"; + const DiagnosticSeverity Severity = DiagnosticSeverity.Error; + string message = $"Listener method has an invalid signature, it should only have a ref {method.messageTypeSymbol.Name} parameter"; + Diagnostic diagnostic = Diagnostic.Create(ID, Category, message, Severity, Severity, true, 0, false, location: method.methodDeclaration.GetLocation()); + context.ReportDiagnostic(diagnostic); + continue; + } + source.Append(GlobalSimulatorTypeName); source.Append(".Register<"); source.Append(method.messageTypeSymbol.ToDisplayString()); source.Append(">("); - source.Append(method.declaringTypeName); + source.Append(method.methodSymbol.ContainingType.ToDisplayString()); source.Append("."); source.Append(method.methodDeclaration.Identifier.Text); source.Append(");"); diff --git a/generator/ListenerMethod.cs b/generator/ListenerMethod.cs index 7c7912c..50e45bd 100644 --- a/generator/ListenerMethod.cs +++ b/generator/ListenerMethod.cs @@ -5,13 +5,13 @@ namespace Simulation { internal class ListenerMethod { - public readonly string declaringTypeName; + public readonly IMethodSymbol methodSymbol; public readonly MethodDeclarationSyntax methodDeclaration; public readonly ITypeSymbol messageTypeSymbol; - public ListenerMethod(string declaringTypeName, MethodDeclarationSyntax methodDeclaration, ITypeSymbol messageTypeSymbol) + public ListenerMethod(IMethodSymbol methodSymbol, MethodDeclarationSyntax methodDeclaration, ITypeSymbol messageTypeSymbol) { - this.declaringTypeName = declaringTypeName; + this.methodSymbol = methodSymbol; this.methodDeclaration = methodDeclaration; this.messageTypeSymbol = messageTypeSymbol; } From 1a72999d218c18f3ee1b0973bc61b9d4c47bf162 Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 6 Jun 2025 12:06:01 -0400 Subject: [PATCH 4/4] Update README.md --- README.md | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1bab07a..2c79861 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Test](https://github.com/simulation-tree/simulation/actions/workflows/test.yml/badge.svg)](https://github.com/simulation-tree/simulation/actions/workflows/test.yml) -Library providing a way to organize and update systems with support for message handling. +Library providing a way to broadcast messages to added listeners. ### Running simulators @@ -18,21 +18,16 @@ public static void Main() simulator.Remove(); } -public class ProgramSystems : ISystem, IDisposable +public class ProgramSystems : IDisposable { public ProgramSystems() { - //initialize + //before addition } public void Dispose() { - //clean up - } - - void ISystem.Update(Simulator simulator, double deltaTime) - { - //do work + //after removal } } ``` @@ -50,7 +45,6 @@ public partial class ListenerSystem : IListener { void IListener.Receive(ref float message) { - //do something with this } } ``` @@ -76,6 +70,32 @@ public struct LoadRequest } ``` +### Global simulator + +Another way to have listeners and broadcasting setup, is using the included `GlobalSimulator` type. +This approach is slimmer than with the `Simulator`, at the cost of the listeners being global to the entire +runtime. +```cs +public class Program +{ + public static void Main() + { + GlobalSimulatorLoader.Load(); + GlobalSimulator.Broadcast(32f); + GlobalSimulator.Broadcast(32f); + GlobalSimulator.Broadcast(32f); + } +} + +public static class Systems +{ + [Listener] + public static void Update(ref float message) + { + } +} +``` + ### Contributing and design This library is created for composing behaviour of programs using systems, ideally created by