Table of Contents

Source Generator Development

This module covers building Roslyn source generators for compile-time GPU kernel code generation.

Why Source Generators?

Source generators provide:

  • Compile-time code generation - No runtime reflection
  • Native AOT compatibility - Essential for DotCompute
  • IDE integration - IntelliSense for generated code
  • Build-time validation - Catch errors early

Source Generator Basics

Generator Structure

using Microsoft.CodeAnalysis;

[Generator]
public class MyGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 1. Find relevant syntax nodes
        var candidates = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: IsCandidateSyntax,
                transform: GetSemanticTarget)
            .Where(m => m is not null);

        // 2. Combine with compilation
        var compilation = context.CompilationProvider.Combine(candidates.Collect());

        // 3. Generate source
        context.RegisterSourceOutput(compilation, GenerateSource);
    }

    private static bool IsCandidateSyntax(SyntaxNode node, CancellationToken ct)
    {
        // Quick syntactic check
        return node is MethodDeclarationSyntax method &&
               method.AttributeLists.Count > 0;
    }

    private static MethodInfo? GetSemanticTarget(
        GeneratorSyntaxContext context,
        CancellationToken ct)
    {
        // Semantic analysis
        var method = (MethodDeclarationSyntax)context.Node;
        var symbol = context.SemanticModel.GetDeclaredSymbol(method, ct);

        if (symbol?.GetAttributes().Any(a =>
            a.AttributeClass?.Name == "KernelAttribute") == true)
        {
            return ExtractMethodInfo(symbol);
        }

        return null;
    }

    private static void GenerateSource(
        SourceProductionContext context,
        (Compilation, ImmutableArray<MethodInfo?>) input)
    {
        var (compilation, methods) = input;

        foreach (var method in methods.Where(m => m != null))
        {
            var source = GenerateKernelWrapper(method!);
            context.AddSource($"{method!.Name}_Generated.g.cs", source);
        }
    }
}

Project Configuration

<!-- MyGenerator.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>
</Project>

Kernel Source Generator

Discovering Kernels

public class KernelInfo
{
    public string Name { get; set; } = "";
    public string Namespace { get; set; } = "";
    public string ContainingClass { get; set; } = "";
    public List<ParameterInfo> Parameters { get; set; } = new();
    public KernelOptions Options { get; set; } = new();
}

public class ParameterInfo
{
    public string Name { get; set; } = "";
    public string Type { get; set; } = "";
    public bool IsReadOnly { get; set; }
    public bool IsOutput { get; set; }
}

private static KernelInfo? ExtractKernelInfo(
    GeneratorSyntaxContext context,
    CancellationToken ct)
{
    var method = (MethodDeclarationSyntax)context.Node;
    var symbol = context.SemanticModel.GetDeclaredSymbol(method, ct);

    if (symbol == null) return null;

    // Check for [Kernel] attribute
    var kernelAttr = symbol.GetAttributes()
        .FirstOrDefault(a => a.AttributeClass?.Name == "KernelAttribute");

    if (kernelAttr == null) return null;

    // Extract kernel info
    var info = new KernelInfo
    {
        Name = symbol.Name,
        Namespace = symbol.ContainingNamespace.ToDisplayString(),
        ContainingClass = symbol.ContainingType.Name,
        Options = ExtractOptions(kernelAttr)
    };

    // Extract parameters
    foreach (var param in symbol.Parameters)
    {
        info.Parameters.Add(new ParameterInfo
        {
            Name = param.Name,
            Type = param.Type.ToDisplayString(),
            IsReadOnly = param.Type.Name.StartsWith("ReadOnlySpan"),
            IsOutput = param.Type.Name == "Span"
        });
    }

    return info;
}

Generating Wrapper Code

private static string GenerateKernelWrapper(KernelInfo kernel)
{
    var sb = new StringBuilder();

    sb.AppendLine("// <auto-generated />");
    sb.AppendLine("#nullable enable");
    sb.AppendLine();
    sb.AppendLine($"namespace {kernel.Namespace};");
    sb.AppendLine();
    sb.AppendLine($"partial class {kernel.ContainingClass}");
    sb.AppendLine("{");

    // Generate kernel definition
    sb.AppendLine($"    public static readonly KernelDefinition {kernel.Name}Definition = new()");
    sb.AppendLine("    {");
    sb.AppendLine($"        Name = \"{kernel.Name}\",");
    sb.AppendLine($"        Parameters = new[]");
    sb.AppendLine("        {");

    foreach (var param in kernel.Parameters)
    {
        var direction = param.IsReadOnly ? "Input" : (param.IsOutput ? "Output" : "InOut");
        sb.AppendLine($"            new ParameterDefinition(\"{param.Name}\", typeof({param.Type}), ParameterDirection.{direction}),");
    }

    sb.AppendLine("        },");
    sb.AppendLine($"        Options = new KernelOptions {{ /* ... */ }}");
    sb.AppendLine("    };");

    // Generate execution helper
    sb.AppendLine();
    sb.AppendLine($"    public static async Task Execute{kernel.Name}Async(");
    sb.AppendLine("        IComputeOrchestrator orchestrator,");
    sb.AppendLine("        KernelConfig config,");

    for (int i = 0; i < kernel.Parameters.Count; i++)
    {
        var param = kernel.Parameters[i];
        var comma = i < kernel.Parameters.Count - 1 ? "," : ")";
        sb.AppendLine($"        IBuffer<{GetElementType(param.Type)}> {param.Name}{comma}");
    }

    sb.AppendLine("    {");
    sb.AppendLine($"        await orchestrator.ExecuteKernelAsync({kernel.Name}, config,");

    var bufferArgs = string.Join(", ", kernel.Parameters.Select(p => p.Name));
    sb.AppendLine($"            {bufferArgs});");
    sb.AppendLine("    }");

    sb.AppendLine("}");

    return sb.ToString();
}

Ring Kernel Generator

Discovering Ring Kernels

public class RingKernelInfo
{
    public string Name { get; set; } = "";
    public string KernelId { get; set; } = "";
    public string InputMessageType { get; set; } = "";
    public string OutputMessageType { get; set; } = "";
    public int Capacity { get; set; } = 1024;
    public RingProcessingMode ProcessingMode { get; set; }
}

private static RingKernelInfo? ExtractRingKernelInfo(
    IMethodSymbol method,
    AttributeData attr)
{
    var info = new RingKernelInfo
    {
        Name = method.Name
    };

    // Extract attribute arguments
    foreach (var arg in attr.NamedArguments)
    {
        switch (arg.Key)
        {
            case "KernelId":
                info.KernelId = arg.Value.Value?.ToString() ?? method.Name;
                break;
            case "InputMessageType":
                info.InputMessageType = ((INamedTypeSymbol)arg.Value.Value!).ToDisplayString();
                break;
            case "OutputMessageType":
                info.OutputMessageType = ((INamedTypeSymbol)arg.Value.Value!).ToDisplayString();
                break;
            case "Capacity":
                info.Capacity = (int)arg.Value.Value!;
                break;
            case "ProcessingMode":
                info.ProcessingMode = (RingProcessingMode)(int)arg.Value.Value!;
                break;
        }
    }

    return info;
}

Generating Message Serialization

private static string GenerateMessageSerializer(string messageType, INamedTypeSymbol typeSymbol)
{
    var sb = new StringBuilder();

    sb.AppendLine($"// Serializer for {messageType}");
    sb.AppendLine($"public static class {typeSymbol.Name}Serializer");
    sb.AppendLine("{");

    // Generate serialize method
    sb.AppendLine($"    public static int Serialize({messageType} message, Span<byte> buffer)");
    sb.AppendLine("    {");
    sb.AppendLine("        int offset = 0;");

    foreach (var member in typeSymbol.GetMembers().OfType<IPropertySymbol>())
    {
        var size = GetTypeSize(member.Type);
        sb.AppendLine($"        // {member.Name}: {member.Type}");
        sb.AppendLine($"        MemoryMarshal.Write(buffer.Slice(offset), ref message.{member.Name});");
        sb.AppendLine($"        offset += {size};");
    }

    sb.AppendLine("        return offset;");
    sb.AppendLine("    }");

    // Generate deserialize method
    sb.AppendLine();
    sb.AppendLine($"    public static {messageType} Deserialize(ReadOnlySpan<byte> buffer)");
    sb.AppendLine("    {");
    sb.AppendLine($"        var message = new {messageType}();");
    sb.AppendLine("        int offset = 0;");

    foreach (var member in typeSymbol.GetMembers().OfType<IPropertySymbol>())
    {
        var size = GetTypeSize(member.Type);
        sb.AppendLine($"        message.{member.Name} = MemoryMarshal.Read<{member.Type}>(buffer.Slice(offset));");
        sb.AppendLine($"        offset += {size};");
    }

    sb.AppendLine("        return message;");
    sb.AppendLine("    }");

    sb.AppendLine("}");

    return sb.ToString();
}

Testing Source Generators

Unit Testing with CSharpGeneratorDriver

public class KernelGeneratorTests
{
    [Fact]
    public void GeneratesWrapperForKernelMethod()
    {
        // Arrange
        var source = @"
using DotCompute.Generators.Kernel.Attributes;

namespace Test;

public static partial class MyKernels
{
    [Kernel]
    public static void VectorAdd(
        ReadOnlySpan<float> a,
        ReadOnlySpan<float> b,
        Span<float> result)
    {
        int idx = Kernel.ThreadId.X;
        if (idx < result.Length)
            result[idx] = a[idx] + b[idx];
    }
}";

        // Act
        var result = GenerateAndCompile(source);

        // Assert
        Assert.Empty(result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
        Assert.Contains(result.GeneratedSources, s => s.HintName.Contains("VectorAdd"));

        var generatedCode = result.GeneratedSources
            .First(s => s.HintName.Contains("VectorAdd"))
            .SourceText.ToString();

        Assert.Contains("VectorAddDefinition", generatedCode);
        Assert.Contains("ExecuteVectorAddAsync", generatedCode);
    }

    private static GeneratorDriverRunResult GenerateAndCompile(string source)
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(source);

        var references = new[]
        {
            MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(Span<>).Assembly.Location),
            // Add DotCompute references
        };

        var compilation = CSharpCompilation.Create(
            "TestAssembly",
            new[] { syntaxTree },
            references,
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

        var generator = new KernelSourceGenerator();
        var driver = CSharpGeneratorDriver.Create(generator);

        return driver.RunGenerators(compilation).GetRunResult();
    }
}

Integration Testing

[Fact]
public async Task GeneratedKernelExecutesCorrectly()
{
    // Generated code should compile and execute
    var services = new ServiceCollection();
    services.AddDotComputeRuntime();
    var provider = services.BuildServiceProvider();
    var orchestrator = provider.GetRequiredService<IComputeOrchestrator>();

    var a = new float[] { 1, 2, 3 };
    var b = new float[] { 4, 5, 6 };
    var result = new float[3];

    using var bufferA = orchestrator.CreateBuffer<float>(3);
    using var bufferB = orchestrator.CreateBuffer<float>(3);
    using var bufferResult = orchestrator.CreateBuffer<float>(3);

    await bufferA.CopyFromAsync(a);
    await bufferB.CopyFromAsync(b);

    // Use generated execution helper
    await MyKernels.ExecuteVectorAddAsync(
        orchestrator,
        new KernelConfig { BlockSize = 256, GridSize = 1 },
        bufferA, bufferB, bufferResult);

    await bufferResult.CopyToAsync(result);

    Assert.Equal(5, result[0]);
    Assert.Equal(7, result[1]);
    Assert.Equal(9, result[2]);
}

Debugging Source Generators

Enable Generator Debugging

<!-- In your .csproj -->
<PropertyGroup>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Debug in Visual Studio

  1. Set breakpoint in generator code
  2. Right-click generator project → Debug → Start New Instance
  3. Open project that uses the generator
  4. Build to hit breakpoints

Logging During Generation

private static void GenerateSource(
    SourceProductionContext context,
    (Compilation, ImmutableArray<KernelInfo?>) input)
{
    var (compilation, kernels) = input;

    // Report diagnostic for debugging
    foreach (var kernel in kernels.Where(k => k != null))
    {
        context.ReportDiagnostic(Diagnostic.Create(
            new DiagnosticDescriptor(
                "DCDEBUG001",
                "Generator Debug",
                $"Processing kernel: {kernel!.Name}",
                "Debug",
                DiagnosticSeverity.Info,
                true),
            Location.None));
    }
}

Performance Optimization

Incremental Generation

public void Initialize(IncrementalGeneratorInitializationContext context)
{
    // Use value providers for efficient caching
    var kernels = context.SyntaxProvider
        .ForAttributeWithMetadataName(
            "DotCompute.Generators.Kernel.Attributes.KernelAttribute",
            predicate: (node, _) => node is MethodDeclarationSyntax,
            transform: (ctx, ct) => ExtractKernelInfo(ctx, ct))
        .Where(k => k is not null)
        .Select((k, _) => k!);

    // Register output with value equality for caching
    context.RegisterSourceOutput(kernels, (ctx, kernel) =>
    {
        var source = GenerateKernelWrapper(kernel);
        ctx.AddSource($"{kernel.Name}.g.cs", source);
    });
}

Minimize Allocations

// Use pooled string builders
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
    new DefaultObjectPoolProvider().CreateStringBuilderPool();

private static string GenerateCode(KernelInfo kernel)
{
    var sb = StringBuilderPool.Get();
    try
    {
        // Generate code using sb
        return sb.ToString();
    }
    finally
    {
        StringBuilderPool.Return(sb);
    }
}

Exercises

Exercise 1: Custom Attribute Generator

Create a generator for a [CachedKernel] attribute that generates caching logic.

Exercise 2: Validation Generator

Generate compile-time validation for kernel parameter constraints.

Exercise 3: Documentation Generator

Generate XML documentation from kernel attributes.

Key Takeaways

  1. Incremental generators provide efficient, cached code generation
  2. Semantic analysis extracts type information accurately
  3. Testing is essential - use CSharpGeneratorDriver
  4. Debug with diagnostics when IDE debugging isn't available
  5. Cache generated output to improve build performance

Next Module

Analyzer Development →

Learn to create Roslyn analyzers for GPU code validation.

Further Reading