├── 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