├── global.json ├── Directory.Build.props ├── Directory.Packages.props ├── .gitignore ├── src └── Comptime │ ├── ComptimeAttribute.cs │ ├── Polyfill.cs │ ├── IncludeUsingsAttribute.cs │ ├── IncludeFilesAttribute.cs │ ├── IncludeGeneratorsAttribute.cs │ ├── Comptime.csproj │ ├── CSharpSerializer.cs │ └── ComptimeSourceGenerator.cs ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── LICENSE ├── test └── Comptime.Tests │ ├── Comptime.Tests.csproj │ ├── ComptimeMethodsTests.cs │ ├── ComptimeMethods.cs │ └── CSharpSerializerTests.cs ├── Comptime.sln └── README.md /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "10.0.100", 4 | "rollForward": "latestFeature" 5 | }, 6 | "test": { 7 | "runner": "Microsoft.Testing.Platform" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Latest 4 | enable 5 | enable 6 | true 7 | 8 | 9 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build results 2 | [Bb]in/ 3 | [Oo]bj/ 4 | [Oo]ut/ 5 | [Ll]og/ 6 | [Ll]ogs/ 7 | 8 | # Visual Studio 9 | .vs/ 10 | *.user 11 | *.suo 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # Rider 16 | .idea/ 17 | 18 | # Visual Studio Code 19 | .vscode/ 20 | 21 | # NuGet 22 | *.nupkg 23 | *.snupkg 24 | packages/ 25 | artifacts/ 26 | 27 | # Test Results 28 | TestResults/ 29 | *.trx 30 | 31 | # OS 32 | .DS_Store 33 | Thumbs.db 34 | 35 | # Project specific 36 | *.DotSettings.user 37 | -------------------------------------------------------------------------------- /src/Comptime/ComptimeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Comptime; 2 | 3 | /// 4 | /// Marks a method for compile-time execution and result serialization. 5 | /// The annotated method must be static, parameterless, and return a serializable type. 6 | /// 7 | [System.AttributeUsage(System.AttributeTargets.Method)] 8 | public sealed class ComptimeAttribute : System.Attribute 9 | { 10 | /// 11 | /// Marks the method for compile-time execution. 12 | /// 13 | public ComptimeAttribute() { } 14 | } 15 | -------------------------------------------------------------------------------- /src/Comptime/Polyfill.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | #if NETSTANDARD2_0 5 | 6 | namespace System.Runtime.CompilerServices 7 | { 8 | using System.ComponentModel; 9 | 10 | /// 11 | /// Reserved to be used by the compiler for tracking metadata. 12 | /// This class should not be used by developers in source code. 13 | /// 14 | [EditorBrowsable(EditorBrowsableState.Never)] 15 | internal static class IsExternalInit 16 | { 17 | } 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - 'doc/**' 8 | - 'README.md' 9 | 10 | pull_request: 11 | branches: [ main ] 12 | paths-ignore: 13 | - 'doc/**' 14 | - 'README.md' 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | env: 21 | DOTNET_NOLOGO: true 22 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 23 | 24 | steps: 25 | - uses: actions/checkout@v6 26 | - uses: actions/setup-dotnet@v5 27 | with: 28 | dotnet-version: | 29 | 8.0.x 30 | 10.0.x 31 | global-json-file: global.json 32 | - name: Build 33 | run: dotnet build --configuration Release 34 | - name: Test 35 | run: dotnet test --configuration Release --no-build 36 | -------------------------------------------------------------------------------- /src/Comptime/IncludeUsingsAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Comptime; 2 | 3 | /// 4 | /// Specifies additional using directives to include in the generated source code. 5 | /// 6 | [System.AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Class, AllowMultiple = false)] 7 | public sealed class IncludeUsingsAttribute : System.Attribute 8 | { 9 | /// 10 | /// Gets the namespaces to include as using directives in the generated code. 11 | /// 12 | public string[] Usings { get; } 13 | 14 | /// 15 | /// Specifies additional using directives to include in the generated code. 16 | /// 17 | public IncludeUsingsAttribute(params string[] usings) 18 | { 19 | Usings = usings ?? System.Array.Empty(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Comptime/IncludeFilesAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Comptime; 2 | 3 | /// 4 | /// Specifies additional source files to include in the compilation when executing 5 | /// compile-time methods. 6 | /// 7 | [System.AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Class, AllowMultiple = false)] 8 | public sealed class IncludeFilesAttribute : System.Attribute 9 | { 10 | /// 11 | /// Gets the relative paths or glob patterns of the files to include. 12 | /// 13 | public string[] Files { get; } 14 | 15 | /// 16 | /// Specifies additional source files to include when executing the compile-time method. 17 | /// 18 | public IncludeFilesAttribute(params string[] files) 19 | { 20 | Files = files ?? System.Array.Empty(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Comptime/IncludeGeneratorsAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Comptime; 2 | 3 | /// 4 | /// Specifies source generator assemblies that should be executed on the compilation before 5 | /// emitting the assembly for method execution. 6 | /// 7 | [System.AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Class, AllowMultiple = false)] 8 | public sealed class IncludeGeneratorsAttribute : System.Attribute 9 | { 10 | /// 11 | /// Gets the assembly names of the source generators to run. 12 | /// 13 | public string[] GeneratorAssemblies { get; } 14 | 15 | /// 16 | /// Specifies source generator assemblies to run before emitting the compilation. 17 | /// 18 | public IncludeGeneratorsAttribute(params string[] generatorAssemblies) 19 | { 20 | GeneratorAssemblies = generatorAssemblies ?? System.Array.Empty(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | env: 13 | DOTNET_NOLOGO: true 14 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | - uses: actions/setup-dotnet@v5 19 | with: 20 | dotnet-version: | 21 | 8.0.x 22 | 10.0.x 23 | global-json-file: global.json 24 | 25 | - name: Test 26 | run: dotnet test --configuration Release 27 | 28 | - name: Pack with dotnet 29 | run: | 30 | arrTag=(${GITHUB_REF//\// }) 31 | VERSION="${arrTag[2]}" 32 | VERSION="${VERSION#v}" 33 | echo "$VERSION" 34 | dotnet pack --output artifacts --configuration Release -p:Version=$VERSION -p:ContinuousIntegrationBuild=True 35 | 36 | - name: Push with dotnet 37 | run: dotnet nuget push artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sébastien Ros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/Comptime.Tests/Comptime.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | false 6 | latest-Default 7 | False 8 | Exe 9 | true 10 | true 11 | true 12 | $(BaseIntermediateOutputPath)\GeneratedFiles 13 | 14 | $(InterceptorsNamespaces);Comptime.Tests 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Comptime.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Comptime", "src\Comptime\Comptime.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Comptime.Tests", "test\Comptime.Tests\Comptime.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F23456789012}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/Comptime/Comptime.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | netstandard2.0 6 | true 7 | true 8 | enable 9 | Latest 10 | cs 11 | true 12 | true 13 | true 14 | false 15 | Generated 16 | $(DefineConstants);SOURCE_GENERATOR 17 | 18 | $(NoWarn);RSEXPERIMENTAL002;RS2007;RS2008 19 | 20 | 21 | Comptime 22 | 1.0.0 23 | Your Name 24 | A source generator that executes methods at compile time and serializes results to C# code. 25 | source-generator;compile-time;comptime;roslyn 26 | MIT 27 | 28 | 29 | 30 | 31 | all 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Comptime 2 | 3 | A .NET source generator that executes methods at compile time and serializes their results to C# code. Comptime brings meta-programming capabilities to C#, enabling compile-time code generation and evaluation. 4 | 5 | ## Overview 6 | 7 | Comptime allows you to mark methods with the `[Comptime]` attribute to have them executed during compilation. The return values are serialized into C# source code and used at runtime, eliminating the need for runtime computation of values that can be determined at build time. 8 | 9 | This meta-programming approach enables developers to shift expensive computations from runtime to compile time, resulting in faster application startup and execution. 10 | 11 | ## Features 12 | 13 | - **Compile-time execution**: Methods marked with `[Comptime]` are executed during compilation 14 | - **Method parameters**: Methods can accept parameters with compile-time constant expressions 15 | - **C# serialization**: Results are serialized to valid C# code 16 | - **Supported return types**: 17 | - Primitive types: `int`, `long`, `short`, `byte`, `sbyte`, `uint`, `ulong`, `ushort`, `float`, `double`, `decimal`, `bool`, `char`, `string` 18 | - Collections: `IReadOnlyList`, `IReadOnlyDictionary`, `List`, `Dictionary` 19 | - Note: Arrays are **not** allowed as return types because they are mutable. Use `IReadOnlyList` instead. 20 | - **Supported argument types**: Any expression that doesn't contain variables, including: 21 | - Literals: `42`, `"hello"`, `true` 22 | - Collection initializers: `new List { 1, 2, 3 }`, `new[] { "a", "b", "c" }` 23 | - Expressions: `1 + 2`, `Math.PI * 2` 24 | - Const values and enum members 25 | - **Interceptor-based**: Uses C# interceptors to replace method calls with pre-computed values 26 | 27 | ## Usage 28 | 29 | ### Basic Usage (Parameterless Methods) 30 | 31 | ```csharp 32 | using Comptime; 33 | 34 | public static partial class Constants 35 | { 36 | [Comptime] 37 | public static IReadOnlyList GetPrimeNumbers() 38 | { 39 | // Complex computation that runs at compile time 40 | var primes = new List(); 41 | for (int i = 2; i <= 100; i++) 42 | { 43 | if (IsPrime(i)) 44 | primes.Add(i); 45 | } 46 | return primes; 47 | } 48 | 49 | private static bool IsPrime(int n) { /* ... */ } 50 | } 51 | 52 | // At runtime, calling GetPrimeNumbers() returns the pre-computed list 53 | var primes = Constants.GetPrimeNumbers(); // Returns [2, 3, 5, 7, 11, ...] 54 | ``` 55 | 56 | ### Methods with Parameters 57 | 58 | ```csharp 59 | using Comptime; 60 | 61 | public static partial class Math 62 | { 63 | [Comptime] 64 | public static long Factorial(int n) 65 | { 66 | if (n <= 1) return 1; 67 | long result = 1; 68 | for (int i = 2; i <= n; i++) 69 | result *= i; 70 | return result; 71 | } 72 | 73 | [Comptime] 74 | public static int SumList(IReadOnlyList numbers) 75 | { 76 | return numbers.Sum(); 77 | } 78 | } 79 | 80 | // Each unique argument combination is computed at compile time 81 | var fact5 = Math.Factorial(5); // Pre-computed: 120 82 | var fact10 = Math.Factorial(10); // Pre-computed: 3628800 83 | 84 | // Collection initializers work too! 85 | var sum = Math.SumList(new List { 1, 2, 3, 4, 5 }); // Pre-computed: 15 86 | var sum2 = Math.SumList(new[] { 10, 20, 30 }); // Pre-computed: 60 87 | ``` 88 | 89 | ### Generic Methods 90 | 91 | ```csharp 92 | using Comptime; 93 | 94 | public static partial class Utils 95 | { 96 | [Comptime] 97 | public static int CountItems(IReadOnlyList items) 98 | { 99 | return items.Count; 100 | } 101 | 102 | [Comptime] 103 | public static string JoinStrings(IReadOnlyList strings, string separator) 104 | { 105 | return string.Join(separator, strings); 106 | } 107 | } 108 | 109 | var count = Utils.CountItems(new[] { "a", "b", "c" }); // Pre-computed: 3 110 | var joined = Utils.JoinStrings(new[] { "hello", "world" }, " "); // Pre-computed: "hello world" 111 | ``` 112 | 113 | ## Requirements 114 | 115 | - .NET 8.0 or later 116 | - C# 12 or later (for interceptors support) 117 | 118 | ## Installation 119 | 120 | ```xml 121 | 122 | ``` 123 | 124 | ## How It Works 125 | 126 | 1. The source generator finds methods marked with `[Comptime]` 127 | 2. It identifies all call sites and their arguments 128 | 3. For each unique argument combination, it executes the method at compile time 129 | 4. The return values are serialized to C# literals/expressions 130 | 5. Interceptor methods are generated that return the pre-computed values 131 | 6. At runtime, calls to the original methods are intercepted and return the cached values 132 | 133 | ## Diagnostics 134 | 135 | | Code | Description | 136 | |------|-------------| 137 | | COMPTIME001 | Class must be partial | 138 | | COMPTIME002 | Method must be static | 139 | | COMPTIME004 | Unsupported return type | 140 | | COMPTIME005 | Compilation emit failed | 141 | | COMPTIME006 | Method execution failed | 142 | | COMPTIME007 | Serialization failed | 143 | | COMPTIME011 | Array return type not allowed (use IReadOnlyList) | 144 | | COMPTIME012 | Argument must be a constant (no variables allowed) | 145 | 146 | ## Limitations 147 | 148 | - Methods must be `static` 149 | - The containing class must be `partial` 150 | - Return types must be immutable (arrays are not allowed, use `IReadOnlyList`) 151 | - Method arguments must be compile-time constant expressions (no variables, only literals and expressions of literals) 152 | - Methods cannot have side effects that depend on runtime state 153 | 154 | ## License 155 | 156 | MIT License 157 | -------------------------------------------------------------------------------- /test/Comptime.Tests/ComptimeMethodsTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Comptime.Tests; 4 | 5 | public class ComptimeMethodsTests 6 | { 7 | [Fact] 8 | public void GetPrimeNumbers_ReturnsPrimesUpTo30() 9 | { 10 | var primes = ComptimeMethods.GetPrimeNumbers(); 11 | 12 | Assert.NotNull(primes); 13 | Assert.Equal(new[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 }, primes); 14 | } 15 | 16 | [Fact] 17 | public void GetMagicNumber_Returns42() 18 | { 19 | var number = ComptimeMethods.GetMagicNumber(); 20 | 21 | Assert.Equal(42, number); 22 | } 23 | 24 | [Fact] 25 | public void GetGreeting_ReturnsHelloWorld() 26 | { 27 | var greeting = ComptimeMethods.GetGreeting(); 28 | 29 | Assert.Equal("Hello, World!", greeting); 30 | } 31 | 32 | [Fact] 33 | public void IsDebugMode_ReturnsFalse() 34 | { 35 | var isDebug = ComptimeMethods.IsDebugMode(); 36 | 37 | Assert.False(isDebug); 38 | } 39 | 40 | [Fact] 41 | public void GetPi_ReturnsApproximatePi() 42 | { 43 | var pi = ComptimeMethods.GetPi(); 44 | 45 | Assert.Equal(3.14159265358979, pi, 10); 46 | } 47 | 48 | [Fact] 49 | public void GetDaysOfWeek_ReturnsAllDays() 50 | { 51 | var days = ComptimeMethods.GetDaysOfWeek(); 52 | 53 | Assert.NotNull(days); 54 | Assert.Equal(7, days.Count); 55 | Assert.Equal("Monday", days[0]); 56 | Assert.Equal("Sunday", days[6]); 57 | } 58 | 59 | [Fact] 60 | public void GetFibonacciNumbers_ReturnsFirst10FibonacciNumbers() 61 | { 62 | var fib = ComptimeMethods.GetFibonacciNumbers(); 63 | 64 | Assert.NotNull(fib); 65 | Assert.Equal(10, fib.Count); 66 | Assert.Equal(new[] { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 }, fib); 67 | } 68 | 69 | [Fact] 70 | public void GetMonthNumbers_ReturnsAllMonths() 71 | { 72 | var months = ComptimeMethods.GetMonthNumbers(); 73 | 74 | Assert.NotNull(months); 75 | Assert.Equal(12, months.Count); 76 | Assert.Equal(1, months["January"]); 77 | Assert.Equal(12, months["December"]); 78 | } 79 | 80 | [Fact] 81 | public void GetSpecialStrings_HandlesEscapeSequences() 82 | { 83 | var strings = ComptimeMethods.GetSpecialStrings(); 84 | 85 | Assert.NotNull(strings); 86 | Assert.Equal(5, strings.Count); 87 | Assert.Equal("Hello\nWorld", strings[0]); 88 | Assert.Equal("Tab\tSeparated", strings[1]); 89 | Assert.Equal("Quote\"Inside", strings[2]); 90 | Assert.Equal("Backslash\\Path", strings[3]); 91 | Assert.Equal("Null\0Char", strings[4]); 92 | } 93 | 94 | [Fact] 95 | public void GetVowels_ReturnsAllVowels() 96 | { 97 | var vowels = ComptimeMethods.GetVowels(); 98 | 99 | Assert.NotNull(vowels); 100 | Assert.Equal(new[] { 'a', 'e', 'i', 'o', 'u' }, vowels); 101 | } 102 | 103 | [Fact] 104 | public void Factorial_ReturnsCorrectValue() 105 | { 106 | Assert.Equal(1L, ComptimeMethods.Factorial(0)); 107 | Assert.Equal(1L, ComptimeMethods.Factorial(1)); 108 | Assert.Equal(2L, ComptimeMethods.Factorial(2)); 109 | Assert.Equal(6L, ComptimeMethods.Factorial(3)); 110 | Assert.Equal(120L, ComptimeMethods.Factorial(5)); 111 | Assert.Equal(3628800L, ComptimeMethods.Factorial(10)); 112 | } 113 | 114 | [Fact] 115 | public void Fibonacci_ReturnsCorrectValue() 116 | { 117 | Assert.Equal(0, ComptimeMethods.Fibonacci(0)); 118 | Assert.Equal(1, ComptimeMethods.Fibonacci(1)); 119 | Assert.Equal(1, ComptimeMethods.Fibonacci(2)); 120 | Assert.Equal(2, ComptimeMethods.Fibonacci(3)); 121 | Assert.Equal(5, ComptimeMethods.Fibonacci(5)); 122 | Assert.Equal(55, ComptimeMethods.Fibonacci(10)); 123 | } 124 | 125 | [Fact] 126 | public void Greet_ReturnsCorrectMessage() 127 | { 128 | Assert.Equal("Hello, World!", ComptimeMethods.Greet("World")); 129 | Assert.Equal("Hello, Alice!", ComptimeMethods.Greet("Alice")); 130 | Assert.Equal("Hello, Bob!", ComptimeMethods.Greet("Bob")); 131 | } 132 | 133 | [Fact] 134 | public void Add_ReturnsCorrectSum() 135 | { 136 | Assert.Equal(3, ComptimeMethods.Add(1, 2)); 137 | Assert.Equal(0, ComptimeMethods.Add(0, 0)); 138 | Assert.Equal(100, ComptimeMethods.Add(50, 50)); 139 | Assert.Equal(-5, ComptimeMethods.Add(-10, 5)); 140 | } 141 | 142 | [Fact] 143 | public void SumList_WithListInitializer_ReturnsSum() 144 | { 145 | // Test with list initializer syntax 146 | Assert.Equal(15, ComptimeMethods.SumList(new List { 1, 2, 3, 4, 5 })); 147 | Assert.Equal(0, ComptimeMethods.SumList(new List())); 148 | Assert.Equal(100, ComptimeMethods.SumList(new List { 100 })); 149 | } 150 | 151 | [Fact] 152 | public void SumList_WithArrayInitializer_ReturnsSum() 153 | { 154 | // Test with array initializer syntax 155 | Assert.Equal(6, ComptimeMethods.SumList(new[] { 1, 2, 3 })); 156 | Assert.Equal(10, ComptimeMethods.SumList(new int[] { 1, 2, 3, 4 })); 157 | } 158 | 159 | [Fact] 160 | public void CountItems_ReturnsCorrectCount() 161 | { 162 | Assert.Equal(3, ComptimeMethods.CountItems(new List { 1, 2, 3 })); 163 | Assert.Equal(5, ComptimeMethods.CountItems(new[] { "a", "b", "c", "d", "e" })); 164 | Assert.Equal(0, ComptimeMethods.CountItems(new List())); 165 | } 166 | 167 | [Fact] 168 | public void JoinStrings_ReturnsJoinedString() 169 | { 170 | Assert.Equal("a,b,c", ComptimeMethods.JoinStrings(new[] { "a", "b", "c" }, ",")); 171 | Assert.Equal("hello world", ComptimeMethods.JoinStrings(new List { "hello", "world" }, " ")); 172 | Assert.Equal("abc", ComptimeMethods.JoinStrings(new[] { "a", "b", "c" }, "")); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /test/Comptime.Tests/ComptimeMethods.cs: -------------------------------------------------------------------------------- 1 | namespace Comptime.Tests; 2 | 3 | /// 4 | /// Contains methods marked with [Comptime] that will be executed at compile time 5 | /// and have their results serialized to C# code. 6 | /// 7 | public static partial class ComptimeMethods 8 | { 9 | /// 10 | /// Returns a list of prime numbers computed at compile time. 11 | /// 12 | [Comptime] 13 | public static IReadOnlyList GetPrimeNumbers() 14 | { 15 | var primes = new List(); 16 | for (int i = 2; i <= 30; i++) 17 | { 18 | if (IsPrime(i)) 19 | { 20 | primes.Add(i); 21 | } 22 | } 23 | return primes; 24 | } 25 | 26 | private static bool IsPrime(int n) 27 | { 28 | if (n < 2) return false; 29 | if (n == 2) return true; 30 | if (n % 2 == 0) return false; 31 | for (int i = 3; i * i <= n; i += 2) 32 | { 33 | if (n % i == 0) return false; 34 | } 35 | return true; 36 | } 37 | 38 | /// 39 | /// Returns a simple integer computed at compile time. 40 | /// 41 | [Comptime] 42 | public static int GetMagicNumber() 43 | { 44 | return 42; 45 | } 46 | 47 | /// 48 | /// Returns a string computed at compile time. 49 | /// 50 | [Comptime] 51 | public static string GetGreeting() 52 | { 53 | return "Hello, World!"; 54 | } 55 | 56 | /// 57 | /// Returns a boolean computed at compile time. 58 | /// 59 | [Comptime] 60 | public static bool IsDebugMode() 61 | { 62 | return false; 63 | } 64 | 65 | /// 66 | /// Returns a double computed at compile time. 67 | /// 68 | [Comptime] 69 | public static double GetPi() 70 | { 71 | return 3.14159265358979; 72 | } 73 | 74 | /// 75 | /// Returns a list of strings computed at compile time. 76 | /// 77 | [Comptime] 78 | public static IReadOnlyList GetDaysOfWeek() 79 | { 80 | return new[] { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }; 81 | } 82 | 83 | /// 84 | /// Returns a list (serialized as array) computed at compile time. 85 | /// 86 | [Comptime] 87 | public static IReadOnlyList GetFibonacciNumbers() 88 | { 89 | var fib = new List { 1, 1 }; 90 | for (int i = 2; i < 10; i++) 91 | { 92 | fib.Add(fib[i - 1] + fib[i - 2]); 93 | } 94 | return fib; 95 | } 96 | 97 | /// 98 | /// Returns a dictionary computed at compile time. 99 | /// 100 | [Comptime] 101 | public static IReadOnlyDictionary GetMonthNumbers() 102 | { 103 | return new Dictionary 104 | { 105 | { "January", 1 }, 106 | { "February", 2 }, 107 | { "March", 3 }, 108 | { "April", 4 }, 109 | { "May", 5 }, 110 | { "June", 6 }, 111 | { "July", 7 }, 112 | { "August", 8 }, 113 | { "September", 9 }, 114 | { "October", 10 }, 115 | { "November", 11 }, 116 | { "December", 12 } 117 | }; 118 | } 119 | 120 | /// 121 | /// Returns a list with special characters in strings. 122 | /// 123 | [Comptime] 124 | public static IReadOnlyList GetSpecialStrings() 125 | { 126 | return new[] 127 | { 128 | "Hello\nWorld", 129 | "Tab\tSeparated", 130 | "Quote\"Inside", 131 | "Backslash\\Path", 132 | "Null\0Char" 133 | }; 134 | } 135 | 136 | /// 137 | /// Returns a list of chars. 138 | /// 139 | [Comptime] 140 | public static IReadOnlyList GetVowels() 141 | { 142 | return new[] { 'a', 'e', 'i', 'o', 'u' }; 143 | } 144 | 145 | /// 146 | /// Returns the factorial of n computed at compile time. 147 | /// 148 | [Comptime] 149 | public static long Factorial(int n) 150 | { 151 | if (n <= 1) return 1; 152 | long result = 1; 153 | for (int i = 2; i <= n; i++) 154 | { 155 | result *= i; 156 | } 157 | return result; 158 | } 159 | 160 | /// 161 | /// Returns the nth Fibonacci number computed at compile time. 162 | /// 163 | [Comptime] 164 | public static int Fibonacci(int n) 165 | { 166 | if (n <= 0) return 0; 167 | if (n == 1) return 1; 168 | 169 | int a = 0, b = 1; 170 | for (int i = 2; i <= n; i++) 171 | { 172 | int temp = a + b; 173 | a = b; 174 | b = temp; 175 | } 176 | return b; 177 | } 178 | 179 | /// 180 | /// Returns a greeting message with the given name computed at compile time. 181 | /// 182 | [Comptime] 183 | public static string Greet(string name) 184 | { 185 | return $"Hello, {name}!"; 186 | } 187 | 188 | /// 189 | /// Returns the sum of two numbers computed at compile time. 190 | /// 191 | [Comptime] 192 | public static int Add(int a, int b) 193 | { 194 | return a + b; 195 | } 196 | 197 | /// 198 | /// Returns the sum of all numbers in a list computed at compile time. 199 | /// 200 | [Comptime] 201 | public static int SumList(IReadOnlyList numbers) 202 | { 203 | return numbers.Sum(); 204 | } 205 | 206 | /// 207 | /// Returns the count of items in a collection computed at compile time. 208 | /// 209 | [Comptime] 210 | public static int CountItems(IReadOnlyList items) 211 | { 212 | return items.Count; 213 | } 214 | 215 | /// 216 | /// Returns the concatenation of strings computed at compile time. 217 | /// 218 | [Comptime] 219 | public static string JoinStrings(IReadOnlyList strings, string separator) 220 | { 221 | return string.Join(separator, strings); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /test/Comptime.Tests/CSharpSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using Comptime; 3 | 4 | namespace Comptime.Tests; 5 | 6 | /// 7 | /// Tests for the CSharpSerializer class directly. 8 | /// 9 | public class CSharpSerializerTests 10 | { 11 | #region Primitive Types 12 | 13 | [Fact] 14 | public void Serialize_Boolean_True() 15 | { 16 | var result = CSharpSerializer.Serialize(true, typeof(bool)); 17 | Assert.Equal("true", result); 18 | } 19 | 20 | [Fact] 21 | public void Serialize_Boolean_False() 22 | { 23 | var result = CSharpSerializer.Serialize(false, typeof(bool)); 24 | Assert.Equal("false", result); 25 | } 26 | 27 | [Fact] 28 | public void Serialize_Int32_Positive() 29 | { 30 | var result = CSharpSerializer.Serialize(42, typeof(int)); 31 | Assert.Equal("42", result); 32 | } 33 | 34 | [Fact] 35 | public void Serialize_Int32_Negative() 36 | { 37 | var result = CSharpSerializer.Serialize(-123, typeof(int)); 38 | Assert.Equal("-123", result); 39 | } 40 | 41 | [Fact] 42 | public void Serialize_Int64() 43 | { 44 | var result = CSharpSerializer.Serialize(9876543210L, typeof(long)); 45 | Assert.Equal("9876543210L", result); 46 | } 47 | 48 | [Fact] 49 | public void Serialize_UInt32() 50 | { 51 | var result = CSharpSerializer.Serialize(42u, typeof(uint)); 52 | Assert.Equal("42u", result); 53 | } 54 | 55 | [Fact] 56 | public void Serialize_UInt64() 57 | { 58 | var result = CSharpSerializer.Serialize(42UL, typeof(ulong)); 59 | Assert.Equal("42UL", result); 60 | } 61 | 62 | [Fact] 63 | public void Serialize_Byte() 64 | { 65 | var result = CSharpSerializer.Serialize((byte)255, typeof(byte)); 66 | Assert.Equal("(byte)255", result); 67 | } 68 | 69 | [Fact] 70 | public void Serialize_SByte() 71 | { 72 | var result = CSharpSerializer.Serialize((sbyte)-128, typeof(sbyte)); 73 | Assert.Equal("(sbyte)-128", result); 74 | } 75 | 76 | [Fact] 77 | public void Serialize_Int16() 78 | { 79 | var result = CSharpSerializer.Serialize((short)1234, typeof(short)); 80 | Assert.Equal("(short)1234", result); 81 | } 82 | 83 | [Fact] 84 | public void Serialize_UInt16() 85 | { 86 | var result = CSharpSerializer.Serialize((ushort)1234, typeof(ushort)); 87 | Assert.Equal("(ushort)1234", result); 88 | } 89 | 90 | [Fact] 91 | public void Serialize_Float() 92 | { 93 | var result = CSharpSerializer.Serialize(3.14f, typeof(float)); 94 | Assert.Contains("3.14", result); 95 | Assert.EndsWith("f", result); 96 | } 97 | 98 | [Fact] 99 | public void Serialize_Float_NaN() 100 | { 101 | var result = CSharpSerializer.Serialize(float.NaN, typeof(float)); 102 | Assert.Equal("float.NaN", result); 103 | } 104 | 105 | [Fact] 106 | public void Serialize_Float_PositiveInfinity() 107 | { 108 | var result = CSharpSerializer.Serialize(float.PositiveInfinity, typeof(float)); 109 | Assert.Equal("float.PositiveInfinity", result); 110 | } 111 | 112 | [Fact] 113 | public void Serialize_Double() 114 | { 115 | var result = CSharpSerializer.Serialize(3.14159265358979d, typeof(double)); 116 | Assert.Contains("3.14159265358979", result); 117 | Assert.EndsWith("d", result); 118 | } 119 | 120 | [Fact] 121 | public void Serialize_Double_NaN() 122 | { 123 | var result = CSharpSerializer.Serialize(double.NaN, typeof(double)); 124 | Assert.Equal("double.NaN", result); 125 | } 126 | 127 | [Fact] 128 | public void Serialize_Decimal() 129 | { 130 | var result = CSharpSerializer.Serialize(123.45m, typeof(decimal)); 131 | Assert.Equal("123.45m", result); 132 | } 133 | 134 | #endregion 135 | 136 | #region Characters and Strings 137 | 138 | [Fact] 139 | public void Serialize_Char_Simple() 140 | { 141 | var result = CSharpSerializer.Serialize('A', typeof(char)); 142 | Assert.Equal("'A'", result); 143 | } 144 | 145 | [Fact] 146 | public void Serialize_Char_Newline() 147 | { 148 | var result = CSharpSerializer.Serialize('\n', typeof(char)); 149 | Assert.Equal("'\\n'", result); 150 | } 151 | 152 | [Fact] 153 | public void Serialize_Char_Tab() 154 | { 155 | var result = CSharpSerializer.Serialize('\t', typeof(char)); 156 | Assert.Equal("'\\t'", result); 157 | } 158 | 159 | [Fact] 160 | public void Serialize_Char_SingleQuote() 161 | { 162 | var result = CSharpSerializer.Serialize('\'', typeof(char)); 163 | Assert.Equal("'\\''", result); 164 | } 165 | 166 | [Fact] 167 | public void Serialize_Char_Backslash() 168 | { 169 | var result = CSharpSerializer.Serialize('\\', typeof(char)); 170 | Assert.Equal("'\\\\'", result); 171 | } 172 | 173 | [Fact] 174 | public void Serialize_String_Simple() 175 | { 176 | var result = CSharpSerializer.Serialize("Hello", typeof(string)); 177 | Assert.Equal("\"Hello\"", result); 178 | } 179 | 180 | [Fact] 181 | public void Serialize_String_WithQuotes() 182 | { 183 | var result = CSharpSerializer.Serialize("Say \"Hello\"", typeof(string)); 184 | Assert.Equal("\"Say \\\"Hello\\\"\"", result); 185 | } 186 | 187 | [Fact] 188 | public void Serialize_String_WithNewlines() 189 | { 190 | var result = CSharpSerializer.Serialize("Line1\nLine2", typeof(string)); 191 | Assert.Equal("\"Line1\\nLine2\"", result); 192 | } 193 | 194 | [Fact] 195 | public void Serialize_String_WithTabs() 196 | { 197 | var result = CSharpSerializer.Serialize("Col1\tCol2", typeof(string)); 198 | Assert.Equal("\"Col1\\tCol2\"", result); 199 | } 200 | 201 | [Fact] 202 | public void Serialize_String_WithBackslashes() 203 | { 204 | var result = CSharpSerializer.Serialize("C:\\Path\\File", typeof(string)); 205 | Assert.Equal("\"C:\\\\Path\\\\File\"", result); 206 | } 207 | 208 | [Fact] 209 | public void Serialize_Null() 210 | { 211 | var result = CSharpSerializer.Serialize(null, typeof(string)); 212 | Assert.Equal("null", result); 213 | } 214 | 215 | #endregion 216 | 217 | #region Arrays 218 | 219 | [Fact] 220 | public void Serialize_IntArray() 221 | { 222 | var result = CSharpSerializer.Serialize(new[] { 1, 2, 3 }, typeof(int[])); 223 | Assert.Equal("new int[] { 1, 2, 3 }", result); 224 | } 225 | 226 | [Fact] 227 | public void Serialize_IntArray_Empty() 228 | { 229 | var result = CSharpSerializer.Serialize(Array.Empty(), typeof(int[])); 230 | Assert.Equal("new int[] { }", result); 231 | } 232 | 233 | [Fact] 234 | public void Serialize_StringArray() 235 | { 236 | var result = CSharpSerializer.Serialize(new[] { "a", "b", "c" }, typeof(string[])); 237 | Assert.Equal("new string[] { \"a\", \"b\", \"c\" }", result); 238 | } 239 | 240 | [Fact] 241 | public void Serialize_CharArray() 242 | { 243 | var result = CSharpSerializer.Serialize(new[] { 'a', 'b', 'c' }, typeof(char[])); 244 | Assert.Equal("new char[] { 'a', 'b', 'c' }", result); 245 | } 246 | 247 | #endregion 248 | 249 | #region Collections 250 | 251 | [Fact] 252 | public void Serialize_ListOfInt() 253 | { 254 | var list = new List { 1, 2, 3 }; 255 | var result = CSharpSerializer.Serialize(list, typeof(IReadOnlyList)); 256 | Assert.Equal("new int[] { 1, 2, 3 }", result); 257 | } 258 | 259 | [Fact] 260 | public void Serialize_Dictionary() 261 | { 262 | var dict = new Dictionary 263 | { 264 | { "one", 1 }, 265 | { "two", 2 } 266 | }; 267 | var result = CSharpSerializer.Serialize(dict, typeof(IReadOnlyDictionary)); 268 | Assert.Contains("new global::System.Collections.Generic.Dictionary", result); 269 | Assert.Contains("{ \"one\", 1 }", result); 270 | Assert.Contains("{ \"two\", 2 }", result); 271 | } 272 | 273 | #endregion 274 | 275 | #region CanSerialize 276 | 277 | [Theory] 278 | [InlineData(typeof(bool), true)] 279 | [InlineData(typeof(int), true)] 280 | [InlineData(typeof(string), true)] 281 | [InlineData(typeof(int[]), false)] // arrays are not allowed (not immutable) 282 | [InlineData(typeof(string[]), false)] // arrays are not allowed (not immutable) 283 | [InlineData(typeof(List), true)] 284 | [InlineData(typeof(IReadOnlyList), true)] 285 | [InlineData(typeof(Dictionary), true)] 286 | [InlineData(typeof(object), false)] // object is not serializable 287 | [InlineData(typeof(DateTime), false)] // DateTime is not supported 288 | public void CanSerialize_ReturnsExpectedResult(Type type, bool expected) 289 | { 290 | var result = CSharpSerializer.CanSerialize(type); 291 | Assert.Equal(expected, result); 292 | } 293 | 294 | #endregion 295 | 296 | #region GetTypeName 297 | 298 | [Theory] 299 | [InlineData(typeof(bool), "bool")] 300 | [InlineData(typeof(int), "int")] 301 | [InlineData(typeof(string), "string")] 302 | [InlineData(typeof(float), "float")] 303 | [InlineData(typeof(double), "double")] 304 | [InlineData(typeof(decimal), "decimal")] 305 | [InlineData(typeof(char), "char")] 306 | [InlineData(typeof(int[]), "int[]")] 307 | [InlineData(typeof(string[]), "string[]")] 308 | public void GetTypeName_ReturnsExpectedResult(Type type, string expected) 309 | { 310 | var result = CSharpSerializer.GetTypeName(type); 311 | Assert.Equal(expected, result); 312 | } 313 | 314 | #endregion 315 | } 316 | -------------------------------------------------------------------------------- /src/Comptime/CSharpSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Globalization; 3 | using System.Text; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | 6 | namespace Comptime; 7 | 8 | /// 9 | /// Provides serialization of .NET objects to C# source code literals. 10 | /// This class can serialize primitive types, strings, and common collection types 11 | /// into valid C# expressions that recreate those values at runtime. 12 | /// 13 | public static class CSharpSerializer 14 | { 15 | /// 16 | /// Attempts to serialize an object to a C# expression. 17 | /// 18 | /// The value to serialize. 19 | /// The target type for the serialization. 20 | /// The resulting C# expression if successful. 21 | /// An error message if serialization fails. 22 | /// True if serialization succeeded, false otherwise. 23 | public static bool TrySerialize(object? value, Type targetType, out string result, out string? error) 24 | { 25 | result = string.Empty; 26 | error = null; 27 | 28 | try 29 | { 30 | result = Serialize(value, targetType); 31 | return true; 32 | } 33 | catch (NotSupportedException ex) 34 | { 35 | error = ex.Message; 36 | return false; 37 | } 38 | } 39 | 40 | /// 41 | /// Serializes an object to a C# expression. 42 | /// 43 | /// The value to serialize. 44 | /// The target type for the serialization. 45 | /// A C# expression that recreates the value. 46 | /// Thrown when the type cannot be serialized. 47 | public static string Serialize(object? value, Type targetType) 48 | { 49 | if (value is null) 50 | { 51 | return "null"; 52 | } 53 | 54 | var actualType = value.GetType(); 55 | 56 | // Handle primitive types 57 | if (TrySerializePrimitive(value, actualType, out var primitiveResult)) 58 | { 59 | return primitiveResult; 60 | } 61 | 62 | // Handle arrays 63 | if (actualType.IsArray) 64 | { 65 | return SerializeArray((Array)value, actualType.GetElementType()!); 66 | } 67 | 68 | // Handle generic types (collections, dictionaries) 69 | if (actualType.IsGenericType) 70 | { 71 | return SerializeGenericType(value, actualType, targetType); 72 | } 73 | 74 | throw new NotSupportedException($"Type '{actualType.FullName}' is not supported for C# serialization."); 75 | } 76 | 77 | private static bool TrySerializePrimitive(object value, Type type, out string result) 78 | { 79 | result = string.Empty; 80 | 81 | if (type == typeof(bool)) 82 | { 83 | result = (bool)value ? "true" : "false"; 84 | return true; 85 | } 86 | 87 | if (type == typeof(byte)) 88 | { 89 | result = $"(byte){((byte)value).ToString(CultureInfo.InvariantCulture)}"; 90 | return true; 91 | } 92 | 93 | if (type == typeof(sbyte)) 94 | { 95 | result = $"(sbyte){((sbyte)value).ToString(CultureInfo.InvariantCulture)}"; 96 | return true; 97 | } 98 | 99 | if (type == typeof(short)) 100 | { 101 | result = $"(short){((short)value).ToString(CultureInfo.InvariantCulture)}"; 102 | return true; 103 | } 104 | 105 | if (type == typeof(ushort)) 106 | { 107 | result = $"(ushort){((ushort)value).ToString(CultureInfo.InvariantCulture)}"; 108 | return true; 109 | } 110 | 111 | if (type == typeof(int)) 112 | { 113 | result = ((int)value).ToString(CultureInfo.InvariantCulture); 114 | return true; 115 | } 116 | 117 | if (type == typeof(uint)) 118 | { 119 | result = ((uint)value).ToString(CultureInfo.InvariantCulture) + "u"; 120 | return true; 121 | } 122 | 123 | if (type == typeof(long)) 124 | { 125 | result = ((long)value).ToString(CultureInfo.InvariantCulture) + "L"; 126 | return true; 127 | } 128 | 129 | if (type == typeof(ulong)) 130 | { 131 | result = ((ulong)value).ToString(CultureInfo.InvariantCulture) + "UL"; 132 | return true; 133 | } 134 | 135 | if (type == typeof(float)) 136 | { 137 | var f = (float)value; 138 | if (float.IsNaN(f)) { result = "float.NaN"; return true; } 139 | if (float.IsPositiveInfinity(f)) { result = "float.PositiveInfinity"; return true; } 140 | if (float.IsNegativeInfinity(f)) { result = "float.NegativeInfinity"; return true; } 141 | result = f.ToString("G9", CultureInfo.InvariantCulture) + "f"; 142 | return true; 143 | } 144 | 145 | if (type == typeof(double)) 146 | { 147 | var d = (double)value; 148 | if (double.IsNaN(d)) { result = "double.NaN"; return true; } 149 | if (double.IsPositiveInfinity(d)) { result = "double.PositiveInfinity"; return true; } 150 | if (double.IsNegativeInfinity(d)) { result = "double.NegativeInfinity"; return true; } 151 | result = d.ToString("G17", CultureInfo.InvariantCulture) + "d"; 152 | return true; 153 | } 154 | 155 | if (type == typeof(decimal)) 156 | { 157 | result = ((decimal)value).ToString(CultureInfo.InvariantCulture) + "m"; 158 | return true; 159 | } 160 | 161 | if (type == typeof(char)) 162 | { 163 | result = SerializeChar((char)value); 164 | return true; 165 | } 166 | 167 | if (type == typeof(string)) 168 | { 169 | result = SerializeString((string)value); 170 | return true; 171 | } 172 | 173 | return false; 174 | } 175 | 176 | private static string SerializeChar(char c) 177 | { 178 | return SyntaxFactory.Literal(c).ToString(); 179 | } 180 | 181 | private static string SerializeString(string s) 182 | { 183 | return SyntaxFactory.Literal(s).ToString(); 184 | } 185 | 186 | private static string SerializeArray(Array array, Type elementType) 187 | { 188 | var sb = new StringBuilder(); 189 | sb.Append("new "); 190 | sb.Append(GetTypeName(elementType)); 191 | sb.Append("[] { "); 192 | 193 | var first = true; 194 | foreach (var item in array) 195 | { 196 | if (!first) sb.Append(", "); 197 | first = false; 198 | sb.Append(Serialize(item, elementType)); 199 | } 200 | 201 | sb.Append(" }"); 202 | return sb.ToString(); 203 | } 204 | 205 | private static string SerializeGenericType(object value, Type actualType, Type targetType) 206 | { 207 | var genericDef = actualType.GetGenericTypeDefinition(); 208 | var genericArgs = actualType.GetGenericArguments(); 209 | 210 | // Handle List, IList, IReadOnlyList - serialize as array 211 | if (genericDef == typeof(List<>) || 212 | IsAssignableToGenericType(actualType, typeof(IList<>)) || 213 | IsAssignableToGenericType(actualType, typeof(IReadOnlyList<>))) 214 | { 215 | var elementType = genericArgs[0]; 216 | return SerializeListAsArray((IEnumerable)value, elementType, targetType); 217 | } 218 | 219 | // Handle Dictionary, IReadOnlyDictionary 220 | if (genericDef == typeof(Dictionary<,>) || 221 | IsAssignableToGenericType(actualType, typeof(IReadOnlyDictionary<,>))) 222 | { 223 | var keyType = genericArgs[0]; 224 | var valueType = genericArgs[1]; 225 | return SerializeDictionary((IDictionary)value, keyType, valueType, targetType); 226 | } 227 | 228 | throw new NotSupportedException($"Generic type '{actualType.FullName}' is not supported for C# serialization."); 229 | } 230 | 231 | private static string SerializeListAsArray(IEnumerable enumerable, Type elementType, Type targetType) 232 | { 233 | var sb = new StringBuilder(); 234 | sb.Append("new "); 235 | sb.Append(GetTypeName(elementType)); 236 | sb.Append("[] { "); 237 | 238 | var first = true; 239 | foreach (var item in enumerable) 240 | { 241 | if (!first) sb.Append(", "); 242 | first = false; 243 | sb.Append(Serialize(item, elementType)); 244 | } 245 | 246 | sb.Append(" }"); 247 | return sb.ToString(); 248 | } 249 | 250 | private static string SerializeDictionary(IDictionary dictionary, Type keyType, Type valueType, Type targetType) 251 | { 252 | var sb = new StringBuilder(); 253 | 254 | // Determine the target dictionary type 255 | // If target is IReadOnlyDictionary, use Dictionary (FrozenDictionary could be added for .NET 8+) 256 | sb.Append("new global::System.Collections.Generic.Dictionary<"); 257 | sb.Append(GetTypeName(keyType)); 258 | sb.Append(", "); 259 | sb.Append(GetTypeName(valueType)); 260 | sb.Append("> { "); 261 | 262 | var first = true; 263 | foreach (DictionaryEntry entry in dictionary) 264 | { 265 | if (!first) sb.Append(", "); 266 | first = false; 267 | sb.Append("{ "); 268 | sb.Append(Serialize(entry.Key, keyType)); 269 | sb.Append(", "); 270 | sb.Append(Serialize(entry.Value, valueType)); 271 | sb.Append(" }"); 272 | } 273 | 274 | sb.Append(" }"); 275 | return sb.ToString(); 276 | } 277 | 278 | /// 279 | /// Gets the C# type name for a given Type. 280 | /// 281 | public static string GetTypeName(Type type) 282 | { 283 | if (type == typeof(bool)) return "bool"; 284 | if (type == typeof(byte)) return "byte"; 285 | if (type == typeof(sbyte)) return "sbyte"; 286 | if (type == typeof(short)) return "short"; 287 | if (type == typeof(ushort)) return "ushort"; 288 | if (type == typeof(int)) return "int"; 289 | if (type == typeof(uint)) return "uint"; 290 | if (type == typeof(long)) return "long"; 291 | if (type == typeof(ulong)) return "ulong"; 292 | if (type == typeof(float)) return "float"; 293 | if (type == typeof(double)) return "double"; 294 | if (type == typeof(decimal)) return "decimal"; 295 | if (type == typeof(char)) return "char"; 296 | if (type == typeof(string)) return "string"; 297 | if (type == typeof(object)) return "object"; 298 | 299 | if (type.IsArray) 300 | { 301 | return GetTypeName(type.GetElementType()!) + "[]"; 302 | } 303 | 304 | if (type.IsGenericType) 305 | { 306 | var genericDef = type.GetGenericTypeDefinition(); 307 | var args = type.GetGenericArguments(); 308 | var argNames = new string[args.Length]; 309 | for (int i = 0; i < args.Length; i++) 310 | { 311 | argNames[i] = GetTypeName(args[i]); 312 | } 313 | 314 | var baseName = genericDef.FullName ?? genericDef.Name; 315 | var backtickIndex = baseName.IndexOf('`'); 316 | if (backtickIndex > 0) 317 | { 318 | baseName = baseName.Substring(0, backtickIndex); 319 | } 320 | 321 | return $"global::{baseName}<{string.Join(", ", argNames)}>"; 322 | } 323 | 324 | return $"global::{type.FullName ?? type.Name}"; 325 | } 326 | 327 | /// 328 | /// Gets the C# type name from a Roslyn type symbol display string. 329 | /// 330 | public static string GetTypeNameFromSymbol(string symbolDisplayString) 331 | { 332 | // Handle common type aliases 333 | return symbolDisplayString switch 334 | { 335 | "bool" => "bool", 336 | "byte" => "byte", 337 | "sbyte" => "sbyte", 338 | "short" => "short", 339 | "ushort" => "ushort", 340 | "int" => "int", 341 | "uint" => "uint", 342 | "long" => "long", 343 | "ulong" => "ulong", 344 | "float" => "float", 345 | "double" => "double", 346 | "decimal" => "decimal", 347 | "char" => "char", 348 | "string" => "string", 349 | "object" => "object", 350 | _ => symbolDisplayString.StartsWith("global::") ? symbolDisplayString : $"global::{symbolDisplayString}" 351 | }; 352 | } 353 | 354 | private static bool IsAssignableToGenericType(Type givenType, Type genericType) 355 | { 356 | var interfaceTypes = givenType.GetInterfaces(); 357 | foreach (var it in interfaceTypes) 358 | { 359 | if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) 360 | return true; 361 | } 362 | 363 | if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) 364 | return true; 365 | 366 | var baseType = givenType.BaseType; 367 | if (baseType == null) return false; 368 | 369 | return IsAssignableToGenericType(baseType, genericType); 370 | } 371 | 372 | /// 373 | /// Checks if a type can be serialized to C#. 374 | /// 375 | public static bool CanSerialize(Type type) 376 | { 377 | // Check primitive types 378 | if (type == typeof(bool) || type == typeof(byte) || type == typeof(sbyte) || 379 | type == typeof(short) || type == typeof(ushort) || type == typeof(int) || 380 | type == typeof(uint) || type == typeof(long) || type == typeof(ulong) || 381 | type == typeof(float) || type == typeof(double) || type == typeof(decimal) || 382 | type == typeof(char) || type == typeof(string)) 383 | { 384 | return true; 385 | } 386 | 387 | // Arrays are not allowed as return types because they are not immutable 388 | if (type.IsArray) 389 | { 390 | return false; 391 | } 392 | 393 | // Check generic collections 394 | if (type.IsGenericType) 395 | { 396 | var genericDef = type.GetGenericTypeDefinition(); 397 | var genericArgs = type.GetGenericArguments(); 398 | 399 | // List, IList, IReadOnlyList 400 | if (genericDef == typeof(List<>) || 401 | genericDef == typeof(IList<>) || 402 | genericDef == typeof(IReadOnlyList<>) || 403 | genericDef == typeof(IEnumerable<>) || 404 | genericDef == typeof(ICollection<>) || 405 | genericDef == typeof(IReadOnlyCollection<>)) 406 | { 407 | return CanSerialize(genericArgs[0]); 408 | } 409 | 410 | // Dictionary, IReadOnlyDictionary 411 | if (genericDef == typeof(Dictionary<,>) || 412 | genericDef == typeof(IDictionary<,>) || 413 | genericDef == typeof(IReadOnlyDictionary<,>)) 414 | { 415 | return CanSerialize(genericArgs[0]) && CanSerialize(genericArgs[1]); 416 | } 417 | } 418 | 419 | return false; 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/Comptime/ComptimeSourceGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Globalization; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Text; 10 | using Microsoft.CodeAnalysis; 11 | using Microsoft.CodeAnalysis.CSharp; 12 | using Microsoft.CodeAnalysis.CSharp.Syntax; 13 | using Microsoft.CodeAnalysis.Text; 14 | using RoslynCompilation = Microsoft.CodeAnalysis.Compilation; 15 | 16 | namespace Comptime; 17 | 18 | /// 19 | /// Incremental source generator that executes methods marked with [Comptime] at compile time 20 | /// and generates C# code with the serialized results. 21 | /// 22 | /// It looks for static methods annotated with that: 23 | /// - are static 24 | /// - are parameterless 25 | /// - return a serializable type 26 | /// It then: 27 | /// - finds all invocation sites of these methods, 28 | /// - builds a temporary compilation including those methods, 29 | /// - executes the methods to obtain their return values, 30 | /// - serializes those values to C# code, 31 | /// - and emits interceptor methods that return the pre-computed values. 32 | /// 33 | [Generator] 34 | public sealed class ComptimeSourceGenerator : IIncrementalGenerator 35 | { 36 | #region Diagnostic Descriptors 37 | 38 | private static readonly DiagnosticDescriptor ClassNotPartialDescriptor = new( 39 | "COMPTIME001", 40 | "Class must be partial", 41 | "Class '{0}' containing [Comptime] method '{1}' must be declared as partial", 42 | "Comptime", 43 | DiagnosticSeverity.Error, 44 | isEnabledByDefault: true, 45 | description: "Classes containing [Comptime] methods must be declared as partial so that the source generator can add generated code to the class."); 46 | 47 | private static readonly DiagnosticDescriptor MethodNotStaticDescriptor = new( 48 | "COMPTIME002", 49 | "Method must be static", 50 | "[Comptime] method '{0}' must be static", 51 | "Comptime", 52 | DiagnosticSeverity.Error, 53 | isEnabledByDefault: true, 54 | description: "Methods marked with [Comptime] must be static."); 55 | 56 | private static readonly DiagnosticDescriptor UnsupportedReturnTypeDescriptor = new( 57 | "COMPTIME004", 58 | "Unsupported return type", 59 | "[Comptime] method '{0}' has return type '{1}' which cannot be serialized to C#", 60 | "Comptime", 61 | DiagnosticSeverity.Error, 62 | isEnabledByDefault: true, 63 | description: "Methods marked with [Comptime] must return a type that can be serialized to C#."); 64 | 65 | private static readonly DiagnosticDescriptor ArrayReturnTypeNotAllowedDescriptor = new( 66 | "COMPTIME011", 67 | "Array return type not allowed", 68 | "[Comptime] method '{0}' returns an array type '{1}'. Arrays are not allowed because they are mutable; use IReadOnlyList instead.", 69 | "Comptime", 70 | DiagnosticSeverity.Error, 71 | isEnabledByDefault: true, 72 | description: "Methods marked with [Comptime] must not return array types because arrays are mutable. Use IReadOnlyList instead."); 73 | 74 | private static readonly DiagnosticDescriptor ArgumentMustBeConstantDescriptor = new( 75 | "COMPTIME012", 76 | "Argument must be a constant", 77 | "Argument '{0}' to [Comptime] method '{1}' must be a compile-time constant literal or an expression of literals. Variables, method calls, and other non-constant expressions are not allowed.", 78 | "Comptime", 79 | DiagnosticSeverity.Error, 80 | isEnabledByDefault: true, 81 | description: "Arguments to [Comptime] methods must be compile-time constant literals or expressions of literals. Variables, method calls, loops, and other non-constant expressions cannot be evaluated at compile time."); 82 | 83 | private static readonly DiagnosticDescriptor GenerationSucceededDescriptor = new( 84 | "COMPTIME000", 85 | "Compile-time execution succeeded", 86 | "Successfully generated source for method '{0}' with {1} intercepted call site(s)", 87 | "Comptime", 88 | DiagnosticSeverity.Info, 89 | isEnabledByDefault: true, 90 | description: "The source generator successfully executed and generated code for this method."); 91 | 92 | private static readonly DiagnosticDescriptor EmitFailedDescriptor = new( 93 | "COMPTIME005", 94 | "Compilation emit failed", 95 | "Emit failed for method '{0}': {1}", 96 | "Comptime", 97 | DiagnosticSeverity.Error, 98 | isEnabledByDefault: true); 99 | 100 | private static readonly DiagnosticDescriptor ExecutionFailedDescriptor = new( 101 | "COMPTIME006", 102 | "Method execution failed", 103 | "[Comptime] method '{0}' threw an exception during compile-time execution: {1}", 104 | "Comptime", 105 | DiagnosticSeverity.Error, 106 | isEnabledByDefault: true); 107 | 108 | private static readonly DiagnosticDescriptor SerializationFailedDescriptor = new( 109 | "COMPTIME007", 110 | "Serialization failed", 111 | "Could not serialize return value of method '{0}': {1}", 112 | "Comptime", 113 | DiagnosticSeverity.Error, 114 | isEnabledByDefault: true); 115 | 116 | private static readonly DiagnosticDescriptor IncludeFileNotFoundDescriptor = new( 117 | "COMPTIME008", 118 | "Include file not found", 119 | "Could not find included file '{0}' for method '{1}'", 120 | "Comptime", 121 | DiagnosticSeverity.Warning, 122 | isEnabledByDefault: true); 123 | 124 | private static readonly DiagnosticDescriptor MethodNotFoundDescriptor = new( 125 | "COMPTIME009", 126 | "Method not found in emitted assembly", 127 | "Could not find method '{0}' on type '{1}'", 128 | "Comptime", 129 | DiagnosticSeverity.Warning, 130 | isEnabledByDefault: true); 131 | 132 | private static readonly DiagnosticDescriptor GenerationErrorDescriptor = new( 133 | "COMPTIME010", 134 | "Error generating source", 135 | "An error occurred while generating source for method '{0}': {1}", 136 | "Comptime", 137 | DiagnosticSeverity.Error, 138 | isEnabledByDefault: true); 139 | 140 | #endregion 141 | 142 | 143 | /// 144 | /// Initializes the incremental generator to find and process methods annotated with . 145 | /// 146 | public void Initialize(IncrementalGeneratorInitializationContext context) 147 | { 148 | // 1. Find candidate methods syntactically (methods with attributes). 149 | var methodDeclarations = context.SyntaxProvider.CreateSyntaxProvider( 150 | static (node, _) => IsCandidateMethod(node), 151 | static (syntaxContext, _) => GetMethodToGenerate(syntaxContext)) 152 | .Where(static m => m is not null)!; 153 | 154 | // 2. Find invocations of methods (potential calls to [Comptime] methods). 155 | var invocations = context.SyntaxProvider.CreateSyntaxProvider( 156 | static (node, _) => node is InvocationExpressionSyntax, 157 | static (syntaxContext, ct) => GetInvocationToIntercept(syntaxContext, ct)) 158 | .Where(static i => i is not null)!; 159 | 160 | // 3. Capture target framework information for the current compilation. 161 | var targetFramework = context.AnalyzerConfigOptionsProvider 162 | .Select(static (options, _) => 163 | { 164 | options.GlobalOptions.TryGetValue("build_property.TargetFramework", out var tfm); 165 | options.GlobalOptions.TryGetValue("build_property.TargetFrameworkIdentifier", out var identifier); 166 | options.GlobalOptions.TryGetValue("build_property.TargetFrameworkVersion", out var version); 167 | 168 | return (tfm: tfm ?? "", identifier: identifier ?? "", version: version ?? ""); 169 | }); 170 | 171 | // 4. Combine the collected methods with invocations, Compilation, and TFM. 172 | var combinedData = context.CompilationProvider 173 | .Combine(targetFramework) 174 | .Combine(methodDeclarations.Collect()) 175 | .Combine(invocations.Collect()); 176 | 177 | // 5. Register for source output. 178 | context.RegisterSourceOutput(combinedData, static (spc, source) => 179 | { 180 | var (((compilation, tfmInfo), methods), invocationList) = source; 181 | 182 | // Output debug info 183 | spc.AddSource("ComptimeDebugInfo.g.cs", SourceText.From( 184 | "// \n" + 185 | "// TargetFramework: " + (string.IsNullOrEmpty(tfmInfo.tfm) ? "" : tfmInfo.tfm) + "\n" + 186 | "// Methods count: " + (methods.IsDefaultOrEmpty ? "0" : methods.Length.ToString(CultureInfo.InvariantCulture)) + "\n" + 187 | "// Invocations count: " + (invocationList.IsDefaultOrEmpty ? "0" : invocationList.Length.ToString(CultureInfo.InvariantCulture)) + "\n" + 188 | "namespace Comptime.Internal;\n" + 189 | "internal static class DebugInfo { }\n", Encoding.UTF8)); 190 | 191 | if (methods.IsDefaultOrEmpty) 192 | { 193 | return; 194 | } 195 | 196 | // Build a lookup of method symbols to their invocations 197 | var invocationsByMethod = new Dictionary>(StringComparer.Ordinal); 198 | if (!invocationList.IsDefaultOrEmpty) 199 | { 200 | foreach (var inv in invocationList) 201 | { 202 | if (inv is null) continue; 203 | var key = inv.Value.TargetMethodKey; 204 | if (!invocationsByMethod.TryGetValue(key, out var list)) 205 | { 206 | list = new List(); 207 | invocationsByMethod[key] = list; 208 | } 209 | list.Add(inv.Value); 210 | } 211 | } 212 | 213 | foreach (var m in methods) 214 | { 215 | if (m is null) 216 | { 217 | continue; 218 | } 219 | 220 | // Report any validation errors first 221 | if (m.Value.ValidationErrors is not null && m.Value.ValidationArgs is not null) 222 | { 223 | for (int i = 0; i < m.Value.ValidationErrors.Length; i++) 224 | { 225 | spc.ReportDiagnostic(Diagnostic.Create( 226 | m.Value.ValidationErrors[i], 227 | m.Value.AttributeLocation ?? m.Value.Method.Locations.FirstOrDefault(), 228 | m.Value.ValidationArgs[i])); 229 | } 230 | // Skip generation if there are validation errors 231 | continue; 232 | } 233 | 234 | try 235 | { 236 | // Get invocations for this method 237 | var methodKey = GetMethodKey(m.Value.Method); 238 | invocationsByMethod.TryGetValue(methodKey, out var methodInvocations); 239 | 240 | // Report errors for invocations with non-constant arguments and filter them out 241 | var validInvocations = new List(); 242 | if (methodInvocations is not null) 243 | { 244 | foreach (var inv in methodInvocations) 245 | { 246 | if (inv.ErrorMessage is not null) 247 | { 248 | spc.ReportDiagnostic(Diagnostic.Create( 249 | ArgumentMustBeConstantDescriptor, 250 | inv.Location, 251 | inv.ErrorMessage, 252 | m.Value.Method.Name)); 253 | } 254 | else 255 | { 256 | validInvocations.Add(inv); 257 | } 258 | } 259 | } 260 | 261 | GenerateForMethod(spc, compilation, tfmInfo, m.Value, validInvocations); 262 | } 263 | catch (Exception ex) 264 | { 265 | spc.ReportDiagnostic(Diagnostic.Create( 266 | GenerationErrorDescriptor, 267 | m.Value.Method.Locations.FirstOrDefault(), 268 | m.Value.Method.Name, 269 | ex.Message)); 270 | continue; 271 | } 272 | } 273 | }); 274 | } 275 | 276 | private static string GetMethodKey(IMethodSymbol method) 277 | { 278 | return method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + "." + method.Name; 279 | } 280 | 281 | 282 | private static InvocationInfo? GetInvocationToIntercept(GeneratorSyntaxContext context, System.Threading.CancellationToken ct) 283 | { 284 | var invocation = (InvocationExpressionSyntax)context.Node; 285 | var semanticModel = context.SemanticModel; 286 | 287 | // Get the method being called 288 | var symbolInfo = semanticModel.GetSymbolInfo(invocation, ct); 289 | if (symbolInfo.Symbol is not IMethodSymbol methodSymbol) 290 | { 291 | return null; 292 | } 293 | 294 | // Check if the method has [Comptime] attribute 295 | var comptimeAttrSymbol = semanticModel.Compilation.GetTypeByMetadataName("Comptime.ComptimeAttribute"); 296 | if (comptimeAttrSymbol is null) 297 | { 298 | return null; 299 | } 300 | 301 | var hasComptimeAttr = methodSymbol.GetAttributes() 302 | .Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, comptimeAttrSymbol)); 303 | 304 | if (!hasComptimeAttr) 305 | { 306 | return null; 307 | } 308 | 309 | // Collect argument expressions (must not contain any variable references) 310 | var argumentExpressions = new List(); 311 | string? errorMessage = null; 312 | 313 | foreach (var arg in invocation.ArgumentList.Arguments) 314 | { 315 | // Check if the expression contains any variable or parameter references 316 | var variableReference = FindVariableReference(arg.Expression, semanticModel, ct); 317 | if (variableReference is not null) 318 | { 319 | // Expression contains a variable - cannot be evaluated at compile time 320 | errorMessage = variableReference; 321 | break; 322 | } 323 | 324 | // Capture the expression source text for compilation 325 | argumentExpressions.Add(arg.Expression.ToFullString().Trim()); 326 | } 327 | 328 | // If there's an error, return an InvocationInfo with the error 329 | if (errorMessage is not null) 330 | { 331 | var methodKey = methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + "." + methodSymbol.Name; 332 | return new InvocationInfo( 333 | methodKey, 334 | null, 335 | invocation.GetLocation(), 336 | Array.Empty(), 337 | errorMessage); 338 | } 339 | 340 | // Get the interceptable location using Roslyn's API 341 | var interceptableLocation = semanticModel.GetInterceptableLocation(invocation, ct); 342 | if (interceptableLocation is null) 343 | { 344 | return null; 345 | } 346 | 347 | var methodKeySuccess = methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + "." + methodSymbol.Name; 348 | 349 | return new InvocationInfo( 350 | methodKeySuccess, 351 | interceptableLocation.GetInterceptsLocationAttributeSyntax(), 352 | invocation.GetLocation(), 353 | argumentExpressions.ToArray(), 354 | null); 355 | } 356 | 357 | /// 358 | /// Recursively checks if an expression contains any variable or parameter references. 359 | /// Returns the problematic identifier if found, null otherwise. 360 | /// 361 | private static string? FindVariableReference(ExpressionSyntax expression, SemanticModel semanticModel, System.Threading.CancellationToken ct) 362 | { 363 | foreach (var node in expression.DescendantNodesAndSelf()) 364 | { 365 | if (node is IdentifierNameSyntax identifier) 366 | { 367 | var symbol = semanticModel.GetSymbolInfo(identifier, ct).Symbol; 368 | 369 | // Check if it's a variable, parameter, or field that's not a constant 370 | if (symbol is ILocalSymbol || symbol is IParameterSymbol) 371 | { 372 | return identifier.Identifier.Text; 373 | } 374 | 375 | if (symbol is IFieldSymbol field && !field.IsConst) 376 | { 377 | return identifier.Identifier.Text; 378 | } 379 | 380 | // Allow type names, method names, const fields, enum members, etc. 381 | } 382 | } 383 | 384 | return null; 385 | } 386 | 387 | private static Type? GetRuntimeType(ITypeSymbol typeSymbol) 388 | { 389 | return typeSymbol.SpecialType switch 390 | { 391 | SpecialType.System_Boolean => typeof(bool), 392 | SpecialType.System_Byte => typeof(byte), 393 | SpecialType.System_SByte => typeof(sbyte), 394 | SpecialType.System_Int16 => typeof(short), 395 | SpecialType.System_UInt16 => typeof(ushort), 396 | SpecialType.System_Int32 => typeof(int), 397 | SpecialType.System_UInt32 => typeof(uint), 398 | SpecialType.System_Int64 => typeof(long), 399 | SpecialType.System_UInt64 => typeof(ulong), 400 | SpecialType.System_Single => typeof(float), 401 | SpecialType.System_Double => typeof(double), 402 | SpecialType.System_Decimal => typeof(decimal), 403 | SpecialType.System_Char => typeof(char), 404 | SpecialType.System_String => typeof(string), 405 | _ => null 406 | }; 407 | } 408 | 409 | private static bool IsCandidateMethod(SyntaxNode node) 410 | => node is MethodDeclarationSyntax m && m.AttributeLists.Count > 0; 411 | 412 | private static MethodToGenerate? GetMethodToGenerate(GeneratorSyntaxContext context) 413 | { 414 | var methodDecl = (MethodDeclarationSyntax)context.Node; 415 | var semanticModel = context.SemanticModel; 416 | var methodSymbol = semanticModel.GetDeclaredSymbol(methodDecl) as IMethodSymbol; 417 | if (methodSymbol is null) 418 | { 419 | return null; 420 | } 421 | 422 | var compilation = semanticModel.Compilation; 423 | var comptimeAttrSymbol = compilation.GetTypeByMetadataName("Comptime.ComptimeAttribute"); 424 | if (comptimeAttrSymbol is null) 425 | { 426 | return null; 427 | } 428 | 429 | // Check if method has [Comptime] attribute 430 | var hasAttribute = methodSymbol.GetAttributes() 431 | .Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, comptimeAttrSymbol)); 432 | 433 | if (!hasAttribute) 434 | { 435 | return null; 436 | } 437 | 438 | var attrLocation = methodSymbol.GetAttributes() 439 | .FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, comptimeAttrSymbol)) 440 | ?.ApplicationSyntaxReference?.GetSyntax().GetLocation(); 441 | 442 | // Collect validation errors 443 | var validationErrors = new List(); 444 | var validationArgs = new List(); 445 | 446 | // Check if class is partial 447 | var containingType = methodSymbol.ContainingType; 448 | var typeDeclaration = containingType.DeclaringSyntaxReferences 449 | .Select(r => r.GetSyntax()) 450 | .OfType() 451 | .FirstOrDefault(); 452 | 453 | if (typeDeclaration is not null && !typeDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) 454 | { 455 | validationErrors.Add(ClassNotPartialDescriptor); 456 | validationArgs.Add(new object?[] { containingType.Name, methodSymbol.Name }); 457 | } 458 | 459 | // Check if method is static 460 | if (!methodSymbol.IsStatic) 461 | { 462 | validationErrors.Add(MethodNotStaticDescriptor); 463 | validationArgs.Add(new object?[] { methodSymbol.Name }); 464 | } 465 | 466 | // Check for [IncludeFiles] attribute on method and containing class 467 | var includeFilesAttrSymbol = compilation.GetTypeByMetadataName("Comptime.IncludeFilesAttribute"); 468 | var additionalFiles = new List(); 469 | if (includeFilesAttrSymbol is not null) 470 | { 471 | CollectAttributeArrayValues(containingType, includeFilesAttrSymbol, additionalFiles); 472 | CollectAttributeArrayValues(methodSymbol, includeFilesAttrSymbol, additionalFiles); 473 | } 474 | 475 | // Check for [IncludeUsings] attribute on method and containing class 476 | var includeUsingsAttrSymbol = compilation.GetTypeByMetadataName("Comptime.IncludeUsingsAttribute"); 477 | var additionalUsings = new List(); 478 | if (includeUsingsAttrSymbol is not null) 479 | { 480 | CollectAttributeArrayValues(containingType, includeUsingsAttrSymbol, additionalUsings); 481 | CollectAttributeArrayValues(methodSymbol, includeUsingsAttrSymbol, additionalUsings); 482 | } 483 | 484 | // Check for [IncludeGenerators] attribute on method and containing class 485 | var includeGeneratorsAttrSymbol = compilation.GetTypeByMetadataName("Comptime.IncludeGeneratorsAttribute"); 486 | var additionalGenerators = new List(); 487 | if (includeGeneratorsAttrSymbol is not null) 488 | { 489 | CollectAttributeArrayValues(containingType, includeGeneratorsAttrSymbol, additionalGenerators); 490 | CollectAttributeArrayValues(methodSymbol, includeGeneratorsAttrSymbol, additionalGenerators); 491 | } 492 | 493 | return new MethodToGenerate( 494 | methodSymbol, 495 | attrLocation, 496 | additionalFiles.ToArray(), 497 | additionalUsings.ToArray(), 498 | additionalGenerators.ToArray(), 499 | validationErrors.Count > 0 ? validationErrors.ToArray() : null, 500 | validationErrors.Count > 0 ? validationArgs.ToArray() : null); 501 | } 502 | 503 | private static void CollectAttributeArrayValues(ISymbol symbol, INamedTypeSymbol attributeType, List values) 504 | { 505 | var attr = symbol.GetAttributes() 506 | .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); 507 | 508 | if (attr is not null && attr.ConstructorArguments.Length > 0) 509 | { 510 | var arg = attr.ConstructorArguments[0]; 511 | if (arg.Kind == TypedConstantKind.Array) 512 | { 513 | values.AddRange(arg.Values 514 | .Where(v => v.Value is string) 515 | .Select(v => (string)v.Value!)); 516 | } 517 | } 518 | } 519 | 520 | private readonly record struct MethodToGenerate( 521 | IMethodSymbol Method, 522 | Location? AttributeLocation, 523 | string[] AdditionalFiles, 524 | string[] AdditionalUsings, 525 | string[] AdditionalGenerators, 526 | DiagnosticDescriptor[]? ValidationErrors, 527 | object?[][]? ValidationArgs); 528 | 529 | private readonly record struct InvocationInfo( 530 | string TargetMethodKey, 531 | string? InterceptsLocationAttribute, 532 | Location Location, 533 | string[] ArgumentExpressions, 534 | string? ErrorMessage); 535 | 536 | [SuppressMessage("Build", "RS1035", Justification = "The generator must execute methods to produce source output.")] 537 | private static void GenerateForMethod( 538 | SourceProductionContext context, 539 | RoslynCompilation hostCompilation, 540 | (string tfm, string identifier, string version) tfmInfo, 541 | MethodToGenerate methodInfo, 542 | List invocations) 543 | { 544 | var methodSymbol = methodInfo.Method; 545 | 546 | // Get the syntax tree containing this method 547 | var methodSyntaxRef = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault(); 548 | if (methodSyntaxRef is null) 549 | { 550 | return; 551 | } 552 | var originalSyntaxTree = methodSyntaxRef.SyntaxTree; 553 | var methodSyntax = methodSyntaxRef.GetSyntax() as MethodDeclarationSyntax; 554 | 555 | if (methodSyntax is null) 556 | { 557 | return; 558 | } 559 | 560 | var parseOptions = (CSharpParseOptions)originalSyntaxTree.Options; 561 | 562 | // Create compilation for execution 563 | var tempCompilation = hostCompilation; 564 | 565 | // Run additional source generators if specified via [IncludeGenerators] attribute 566 | if (methodInfo.AdditionalGenerators.Length > 0) 567 | { 568 | tempCompilation = RunAdditionalGenerators(context, tempCompilation, methodInfo.AdditionalGenerators, parseOptions, methodSymbol); 569 | } 570 | 571 | using var peStream = new MemoryStream(); 572 | var emitResult = tempCompilation.Emit(peStream); 573 | 574 | if (!emitResult.Success) 575 | { 576 | var errorMessages = string.Join("; ", emitResult.Diagnostics 577 | .Where(d => d.Severity == DiagnosticSeverity.Error) 578 | .Take(5) 579 | .Select(d => 580 | { 581 | var location = d.Location; 582 | var lineSpan = location.GetLineSpan(); 583 | var fileName = Path.GetFileName(lineSpan.Path); 584 | var line = lineSpan.StartLinePosition.Line + 1; 585 | return $"{fileName}({line}): {d.GetMessage(CultureInfo.InvariantCulture)}"; 586 | })); 587 | 588 | context.ReportDiagnostic(Diagnostic.Create( 589 | EmitFailedDescriptor, 590 | methodSymbol.Locations.FirstOrDefault(), 591 | methodSymbol.Name, 592 | errorMessages)); 593 | return; 594 | } 595 | 596 | peStream.Position = 0; 597 | 598 | // Build a dictionary of assembly paths from the compilation's references 599 | var assemblyPaths = new Dictionary(StringComparer.OrdinalIgnoreCase); 600 | var pendingCompilationRefs = new List<(string Name, RoslynCompilation Compilation)>(); 601 | var loadedAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase); 602 | 603 | foreach (var reference in hostCompilation.References) 604 | { 605 | if (reference is PortableExecutableReference peRef && peRef.FilePath is not null) 606 | { 607 | var assemblyName = Path.GetFileNameWithoutExtension(peRef.FilePath); 608 | var filePath = peRef.FilePath; 609 | 610 | // Handle ref assemblies - try to find actual assembly 611 | if (filePath.Contains(Path.DirectorySeparatorChar + "ref" + Path.DirectorySeparatorChar) || 612 | filePath.Contains("/ref/")) 613 | { 614 | var objIndex = filePath.LastIndexOf(Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase); 615 | if (objIndex < 0) objIndex = filePath.LastIndexOf("/obj/", StringComparison.OrdinalIgnoreCase); 616 | 617 | if (objIndex >= 0) 618 | { 619 | var baseDir = filePath.Substring(0, objIndex); 620 | var afterObj = filePath.Substring(objIndex + 4); 621 | var refIndex = afterObj.IndexOf(Path.DirectorySeparatorChar + "ref" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase); 622 | if (refIndex < 0) refIndex = afterObj.IndexOf("/ref/", StringComparison.OrdinalIgnoreCase); 623 | 624 | if (refIndex >= 0) 625 | { 626 | var configTfm = afterObj.Substring(0, refIndex); 627 | var fileName = Path.GetFileName(filePath); 628 | var binPath = baseDir + Path.DirectorySeparatorChar + "bin" + configTfm + Path.DirectorySeparatorChar + fileName; 629 | 630 | if (File.Exists(binPath)) 631 | { 632 | filePath = binPath; 633 | } 634 | } 635 | } 636 | } 637 | 638 | if (!assemblyPaths.ContainsKey(assemblyName)) 639 | { 640 | assemblyPaths[assemblyName] = filePath; 641 | } 642 | } 643 | else if (reference is CompilationReference compRef) 644 | { 645 | var refCompilation = compRef.Compilation; 646 | var refAssemblyName = refCompilation.AssemblyName; 647 | if (refAssemblyName is not null) 648 | { 649 | pendingCompilationRefs.Add((refAssemblyName, refCompilation)); 650 | } 651 | } 652 | } 653 | 654 | // Set up an assembly resolver 655 | ResolveEventHandler? resolver = null; 656 | resolver = (sender, args) => 657 | { 658 | var requestedName = new AssemblyName(args.Name); 659 | 660 | // Try to return pre-loaded assembly from project references 661 | if (requestedName.Name is not null && loadedAssemblies.TryGetValue(requestedName.Name, out var loadedAssembly)) 662 | { 663 | return loadedAssembly; 664 | } 665 | 666 | // Try to load from file-based references 667 | if (requestedName.Name is not null && assemblyPaths.TryGetValue(requestedName.Name, out var path)) 668 | { 669 | try 670 | { 671 | var asm = Assembly.LoadFrom(path); 672 | loadedAssemblies[requestedName.Name] = asm; 673 | return asm; 674 | } 675 | catch 676 | { 677 | // Fall through to return null 678 | } 679 | } 680 | 681 | return null; 682 | }; 683 | 684 | AppDomain.CurrentDomain.AssemblyResolve += resolver; 685 | 686 | // Load CompilationReference assemblies 687 | foreach (var (refAssemblyName, refCompilation) in pendingCompilationRefs) 688 | { 689 | if (!loadedAssemblies.ContainsKey(refAssemblyName)) 690 | { 691 | using var refPeStream = new MemoryStream(); 692 | var refEmitResult = refCompilation.Emit(refPeStream); 693 | if (refEmitResult.Success) 694 | { 695 | refPeStream.Position = 0; 696 | try 697 | { 698 | var refAssembly = Assembly.Load(refPeStream.ToArray()); 699 | loadedAssemblies[refAssemblyName] = refAssembly; 700 | } 701 | catch 702 | { 703 | // Ignore load failures 704 | } 705 | } 706 | } 707 | } 708 | 709 | try 710 | { 711 | var assembly = Assembly.Load(peStream.ToArray()); 712 | 713 | // Locate the generated type and method via reflection 714 | var containingTypeName = methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); 715 | const string globalPrefix = "global::"; 716 | if (containingTypeName.StartsWith(globalPrefix, StringComparison.Ordinal)) 717 | { 718 | containingTypeName = containingTypeName.Substring(globalPrefix.Length); 719 | } 720 | 721 | var type = assembly.GetType(containingTypeName); 722 | if (type is null) 723 | { 724 | return; 725 | } 726 | 727 | // Find the method with matching parameter count 728 | var parameterTypes = methodSymbol.Parameters.Select(p => GetRuntimeType(p.Type)).ToArray(); 729 | MethodInfo? method = null; 730 | 731 | if (parameterTypes.All(t => t is not null)) 732 | { 733 | method = type.GetMethod( 734 | methodSymbol.Name, 735 | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, 736 | null, 737 | parameterTypes!, 738 | null); 739 | } 740 | 741 | // Fallback to finding by name if exact match fails 742 | method ??= type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) 743 | .FirstOrDefault(m => m.Name == methodSymbol.Name && m.GetParameters().Length == methodSymbol.Parameters.Length); 744 | 745 | if (method is null) 746 | { 747 | context.ReportDiagnostic(Diagnostic.Create( 748 | MethodNotFoundDescriptor, 749 | methodSymbol.Locations.FirstOrDefault(), 750 | methodSymbol.Name, 751 | containingTypeName)); 752 | return; 753 | } 754 | 755 | // Get the return type 756 | var returnType = method.ReturnType; 757 | 758 | // Check if the type is an array (not allowed because arrays are mutable) 759 | if (returnType.IsArray) 760 | { 761 | context.ReportDiagnostic(Diagnostic.Create( 762 | ArrayReturnTypeNotAllowedDescriptor, 763 | methodInfo.AttributeLocation ?? methodSymbol.Locations.FirstOrDefault(), 764 | methodSymbol.Name, 765 | returnType.FullName)); 766 | return; 767 | } 768 | 769 | // Check if the type can be serialized 770 | if (!CSharpSerializer.CanSerialize(returnType)) 771 | { 772 | context.ReportDiagnostic(Diagnostic.Create( 773 | UnsupportedReturnTypeDescriptor, 774 | methodInfo.AttributeLocation ?? methodSymbol.Locations.FirstOrDefault(), 775 | methodSymbol.Name, 776 | returnType.FullName)); 777 | return; 778 | } 779 | 780 | // Group invocations by their argument expressions to avoid duplicate execution 781 | var invocationGroups = new List<(string ArgsKey, string[] ArgExpressions, List Invocations)>(); 782 | 783 | foreach (var inv in invocations) 784 | { 785 | // Create a key based on argument expressions 786 | var argsKey = string.Join("|", inv.ArgumentExpressions); 787 | 788 | var existingGroup = invocationGroups.FirstOrDefault(g => g.ArgsKey == argsKey); 789 | if (existingGroup.Invocations is not null) 790 | { 791 | existingGroup.Invocations.Add(inv); 792 | } 793 | else 794 | { 795 | invocationGroups.Add((argsKey, inv.ArgumentExpressions, new List { inv })); 796 | } 797 | } 798 | 799 | // Execute method for each unique argument combination and collect results 800 | var executionResults = new List<(string SerializedValue, string[] ArgExpressions, List Invocations)>(); 801 | 802 | foreach (var (argsKey, argExpressions, groupInvocations) in invocationGroups) 803 | { 804 | object? result; 805 | try 806 | { 807 | // For methods with arguments, we need to invoke with the evaluated expressions 808 | if (argExpressions.Length > 0) 809 | { 810 | // Create a wrapper method that calls the original method with the literal expressions 811 | var wrapperResult = ExecuteMethodWithArguments( 812 | tempCompilation, 813 | parseOptions, 814 | methodSymbol, 815 | argExpressions, 816 | assemblyPaths, 817 | loadedAssemblies, 818 | pendingCompilationRefs); 819 | 820 | if (wrapperResult.Error is not null) 821 | { 822 | context.ReportDiagnostic(Diagnostic.Create( 823 | ExecutionFailedDescriptor, 824 | groupInvocations[0].Location, 825 | methodSymbol.Name, 826 | wrapperResult.Error)); 827 | continue; 828 | } 829 | 830 | result = wrapperResult.Result; 831 | } 832 | else 833 | { 834 | result = method.Invoke(null, null); 835 | } 836 | } 837 | catch (TargetInvocationException ex) 838 | { 839 | context.ReportDiagnostic(Diagnostic.Create( 840 | ExecutionFailedDescriptor, 841 | groupInvocations[0].Location, 842 | methodSymbol.Name, 843 | ex.InnerException?.Message ?? ex.Message)); 844 | continue; 845 | } 846 | catch (Exception ex) 847 | { 848 | context.ReportDiagnostic(Diagnostic.Create( 849 | ExecutionFailedDescriptor, 850 | groupInvocations[0].Location, 851 | methodSymbol.Name, 852 | ex.Message)); 853 | continue; 854 | } 855 | 856 | // Serialize the result to C# 857 | if (!CSharpSerializer.TrySerialize(result, returnType, out var serializedValue, out var serializeError)) 858 | { 859 | context.ReportDiagnostic(Diagnostic.Create( 860 | SerializationFailedDescriptor, 861 | groupInvocations[0].Location, 862 | methodSymbol.Name, 863 | serializeError)); 864 | continue; 865 | } 866 | 867 | executionResults.Add((serializedValue, argExpressions, groupInvocations)); 868 | } 869 | 870 | if (executionResults.Count == 0) 871 | { 872 | return; 873 | } 874 | 875 | // Generate the source code 876 | var sourceText = GenerateSourceCode(methodSymbol, returnType, executionResults, methodInfo.AdditionalUsings); 877 | 878 | var hintName = $"{methodSymbol.ContainingType.Name}_{methodSymbol.Name}.Comptime.g.cs"; 879 | 880 | context.AddSource(hintName, SourceText.From(sourceText, Encoding.UTF8)); 881 | 882 | // Report success 883 | context.ReportDiagnostic(Diagnostic.Create( 884 | GenerationSucceededDescriptor, 885 | methodInfo.AttributeLocation ?? methodSymbol.Locations.FirstOrDefault(), 886 | methodSymbol.Name, 887 | invocations.Count)); 888 | } 889 | finally 890 | { 891 | AppDomain.CurrentDomain.AssemblyResolve -= resolver; 892 | } 893 | } 894 | 895 | private static string GenerateSourceCode( 896 | IMethodSymbol methodSymbol, 897 | Type returnType, 898 | List<(string SerializedValue, string[] ArgExpressions, List Invocations)> executionResults, 899 | string[] additionalUsings) 900 | { 901 | var sb = new StringBuilder(); 902 | 903 | sb.AppendLine("// "); 904 | sb.AppendLine("#nullable enable"); 905 | sb.AppendLine(); 906 | sb.AppendLine("using System;"); 907 | sb.AppendLine("using System.Collections.Generic;"); 908 | sb.AppendLine("using System.Runtime.CompilerServices;"); 909 | 910 | foreach (var u in additionalUsings) 911 | { 912 | sb.AppendLine($"using {u};"); 913 | } 914 | 915 | sb.AppendLine(); 916 | 917 | // File-local InterceptsLocationAttribute to avoid conflicts with other generators 918 | sb.AppendLine("namespace System.Runtime.CompilerServices"); 919 | sb.AppendLine("{"); 920 | sb.AppendLine(" [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]"); 921 | sb.AppendLine(" file sealed class InterceptsLocationAttribute : global::System.Attribute"); 922 | sb.AppendLine(" {"); 923 | sb.AppendLine(" public InterceptsLocationAttribute(int version, string data) { }"); 924 | sb.AppendLine(" }"); 925 | sb.AppendLine("}"); 926 | sb.AppendLine(); 927 | 928 | // Get namespace and class info 929 | var containingType = methodSymbol.ContainingType; 930 | var ns = containingType.ContainingNamespace; 931 | var hasNamespace = !ns.IsGlobalNamespace; 932 | 933 | if (hasNamespace) 934 | { 935 | sb.AppendLine($"namespace {ns.ToDisplayString()}"); 936 | sb.AppendLine("{"); 937 | } 938 | 939 | // Get accessibility modifier 940 | var accessibility = containingType.DeclaredAccessibility switch 941 | { 942 | Accessibility.Public => "public", 943 | Accessibility.Internal => "internal", 944 | Accessibility.Private => "private", 945 | Accessibility.Protected => "protected", 946 | Accessibility.ProtectedOrInternal => "protected internal", 947 | Accessibility.ProtectedAndInternal => "private protected", 948 | _ => "internal" 949 | }; 950 | 951 | var indent = hasNamespace ? " " : ""; 952 | 953 | // Generate partial class 954 | sb.AppendLine($"{indent}{accessibility} static partial class {containingType.Name}"); 955 | sb.AppendLine($"{indent}{{"); 956 | 957 | var returnTypeName = CSharpSerializer.GetTypeName(returnType); 958 | var methodAccessibility = methodSymbol.DeclaredAccessibility switch 959 | { 960 | Accessibility.Public => "public", 961 | Accessibility.Internal => "internal", 962 | Accessibility.Private => "private", 963 | Accessibility.Protected => "protected", 964 | Accessibility.ProtectedOrInternal => "protected internal", 965 | Accessibility.ProtectedAndInternal => "private protected", 966 | _ => "internal" 967 | }; 968 | 969 | // Build parameter list for interceptor methods using the symbol's type names 970 | var parameterList = string.Join(", ", methodSymbol.Parameters.Select(p => 971 | $"{p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {p.Name}")); 972 | 973 | // Build type parameter list for generic methods 974 | var typeParameterList = methodSymbol.TypeParameters.Length > 0 975 | ? "<" + string.Join(", ", methodSymbol.TypeParameters.Select(tp => tp.Name)) + ">" 976 | : ""; 977 | 978 | // Generate a field and interceptor for each unique argument combination 979 | var counter = 0; 980 | foreach (var (serializedValue, argExpressions, invocations) in executionResults) 981 | { 982 | var suffix = counter == 0 ? "" : $"_{counter}"; 983 | var fieldName = $"_comptime_{methodSymbol.Name}{suffix}"; 984 | 985 | // Generate the cached field 986 | sb.AppendLine($"{indent} private static readonly {returnTypeName} {fieldName} = {serializedValue};"); 987 | sb.AppendLine(); 988 | 989 | // Generate interceptor method with InterceptsLocation attributes for this group 990 | foreach (var invocation in invocations) 991 | { 992 | sb.AppendLine($"{indent} {invocation.InterceptsLocationAttribute}"); 993 | } 994 | 995 | sb.AppendLine($"{indent} {methodAccessibility} static {returnTypeName} {methodSymbol.Name}_Intercepted{suffix}{typeParameterList}({parameterList})"); 996 | sb.AppendLine($"{indent} {{"); 997 | sb.AppendLine($"{indent} return {fieldName};"); 998 | sb.AppendLine($"{indent} }}"); 999 | sb.AppendLine(); 1000 | 1001 | counter++; 1002 | } 1003 | 1004 | sb.AppendLine($"{indent}}}"); 1005 | 1006 | if (hasNamespace) 1007 | { 1008 | sb.AppendLine("}"); 1009 | } 1010 | 1011 | return sb.ToString(); 1012 | } 1013 | 1014 | [SuppressMessage("Build", "RS1035", Justification = "The generator must execute methods to produce source output.")] 1015 | private static (object? Result, string? Error) ExecuteMethodWithArguments( 1016 | RoslynCompilation compilation, 1017 | CSharpParseOptions parseOptions, 1018 | IMethodSymbol methodSymbol, 1019 | string[] argExpressions, 1020 | Dictionary assemblyPaths, 1021 | Dictionary loadedAssemblies, 1022 | List<(string Name, RoslynCompilation Compilation)> pendingCompilationRefs) 1023 | { 1024 | // Build the fully qualified method call 1025 | var containingTypeName = methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); 1026 | var argsString = string.Join(", ", argExpressions); 1027 | 1028 | // Create a wrapper class with a method that calls the target method with the literal arguments 1029 | var wrapperCode = $@" 1030 | using System; 1031 | using System.Collections.Generic; 1032 | using System.Linq; 1033 | 1034 | namespace ComptimeWrapper 1035 | {{ 1036 | public static class Wrapper 1037 | {{ 1038 | public static object Execute() 1039 | {{ 1040 | return {containingTypeName}.{methodSymbol.Name}({argsString}); 1041 | }} 1042 | }} 1043 | }} 1044 | "; 1045 | 1046 | var wrapperTree = CSharpSyntaxTree.ParseText(wrapperCode, parseOptions); 1047 | var wrapperCompilation = compilation.AddSyntaxTrees(wrapperTree); 1048 | 1049 | using var peStream = new MemoryStream(); 1050 | var emitResult = wrapperCompilation.Emit(peStream); 1051 | 1052 | if (!emitResult.Success) 1053 | { 1054 | var errors = string.Join("; ", emitResult.Diagnostics 1055 | .Where(d => d.Severity == DiagnosticSeverity.Error) 1056 | .Take(3) 1057 | .Select(d => d.GetMessage(CultureInfo.InvariantCulture))); 1058 | return (null, $"Failed to compile argument expressions: {errors}"); 1059 | } 1060 | 1061 | peStream.Position = 0; 1062 | 1063 | try 1064 | { 1065 | var assembly = Assembly.Load(peStream.ToArray()); 1066 | var wrapperType = assembly.GetType("ComptimeWrapper.Wrapper"); 1067 | if (wrapperType is null) 1068 | { 1069 | return (null, "Could not find wrapper type"); 1070 | } 1071 | 1072 | var executeMethod = wrapperType.GetMethod("Execute", BindingFlags.Public | BindingFlags.Static); 1073 | if (executeMethod is null) 1074 | { 1075 | return (null, "Could not find Execute method"); 1076 | } 1077 | 1078 | var result = executeMethod.Invoke(null, null); 1079 | return (result, null); 1080 | } 1081 | catch (TargetInvocationException ex) 1082 | { 1083 | return (null, ex.InnerException?.Message ?? ex.Message); 1084 | } 1085 | catch (Exception ex) 1086 | { 1087 | return (null, ex.Message); 1088 | } 1089 | } 1090 | 1091 | private static RoslynCompilation RunAdditionalGenerators( 1092 | SourceProductionContext context, 1093 | RoslynCompilation compilation, 1094 | string[] generatorAssemblies, 1095 | CSharpParseOptions parseOptions, 1096 | IMethodSymbol methodSymbol) 1097 | { 1098 | var generators = new List(); 1099 | 1100 | foreach (var genAssemblyName in generatorAssemblies) 1101 | { 1102 | // Find the generator assembly in analyzer references 1103 | foreach (var reference in compilation.References) 1104 | { 1105 | if (reference is PortableExecutableReference peRef && peRef.FilePath is not null) 1106 | { 1107 | var refName = Path.GetFileNameWithoutExtension(peRef.FilePath); 1108 | if (string.Equals(refName, genAssemblyName, StringComparison.OrdinalIgnoreCase)) 1109 | { 1110 | try 1111 | { 1112 | var genAssembly = Assembly.LoadFrom(peRef.FilePath); 1113 | var genTypes = genAssembly.GetTypes() 1114 | .Where(t => typeof(ISourceGenerator).IsAssignableFrom(t) && !t.IsAbstract); 1115 | 1116 | foreach (var genType in genTypes) 1117 | { 1118 | if (Activator.CreateInstance(genType) is ISourceGenerator gen) 1119 | { 1120 | generators.Add(gen); 1121 | } 1122 | } 1123 | } 1124 | catch 1125 | { 1126 | // Ignore load failures 1127 | } 1128 | } 1129 | } 1130 | } 1131 | } 1132 | 1133 | if (generators.Count > 0) 1134 | { 1135 | var driver = CSharpGeneratorDriver.Create(generators.ToArray()) 1136 | .WithUpdatedParseOptions(parseOptions); 1137 | 1138 | driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out _); 1139 | return updatedCompilation; 1140 | } 1141 | 1142 | return compilation; 1143 | } 1144 | } 1145 | --------------------------------------------------------------------------------