diff --git a/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs b/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs index 1d1459093..0ebdc716e 100644 --- a/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs +++ b/src/ComputeSharp.D2D1.SourceGenerators/D2DPixelShaderDescriptorGenerator.cs @@ -26,8 +26,11 @@ public sealed partial class D2DPixelShaderDescriptorGenerator : IIncrementalGene /// public void Initialize(IncrementalGeneratorInitializationContext context) { - // Discover all shader types and extract all the necessary info from each of them - IncrementalValuesProvider shaderInfo = + // Discover all shader types and extract all the necessary info from each of them (with the exception + // of the compiled HLSL bytecode, which is processed later). The info needed to synthesize the diagnostics + // for the deferred compilation is returned alongside each model (as it holds syntax tree references, it + // is intentionally not part of the model itself, so it never affects the equality of the shader models). + IncrementalValuesProvider<(D2D1ShaderInfo Info, HlslBytecodeDiagnosticsInfo DiagnosticsInfo)> shaderInfoWithDiagnosticsInfo = context.SyntaxProvider .ForAttributeWithMetadataName( "ComputeSharp.D2D1.D2DGeneratedPixelShaderDescriptorAttribute", @@ -152,22 +155,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - // As the last steps in the pipeline, try to compile the shader if needed. - // This is done last so that it can be skipped if any errors happened before. + // Prepare the key to compile the shader afterwards. The compilation is deliberately not + // done here: the incremental driver invokes transform callbacks sequentially, so compiling + // here would serialize all shader compilations. Instead, compilation is deferred to a + // dedicated node below, which can process all shaders in the compilation in parallel. HlslBytecodeInfoKey hlslInfoKey = new( hlslSource, effectiveShaderProfile, effectiveCompileOptions, isCompilationEnabled); - // Get the existing compiled shader, or compile the processed HLSL code - HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token); - - token.ThrowIfCancellationRequested(); - - // Append any diagnostic for the shader compilation - HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(typeSymbol, hlslInfo, diagnostics); - HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(typeSymbol, hlslInfo, diagnostics); + // Capture the info needed to synthesize the diagnostics for the deferred compilation, + // as they cannot be created later (symbols must not be used past the transform node) + HlslBytecodeDiagnosticsInfo hlslDiagnosticsInfo = HlslBytecodeSyntaxProcessor.GetDiagnosticsInfo(typeSymbol); token.ThrowIfCancellationRequested(); @@ -176,7 +176,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - return new D2D1ShaderInfo( + D2D1ShaderInfo info = new( Hierarchy: hierarchyInfo, EffectId: effectId, EffectDisplayName: effectDisplayName, @@ -192,11 +192,62 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ChannelDepth: channelDepth, PixelOptions: pixelOptions, HlslInfoKey: hlslInfoKey, - HlslInfo: hlslInfo, + HlslInfo: HlslBytecodeInfo.Missing.Instance, Diagnostcs: diagnostics.ToImmutable()); + + return (Info: info, DiagnosticsInfo: hlslDiagnosticsInfo); }) - .WithTrackingName(WellKnownTrackingNames.Execute) - .Where(static item => item is not null)!; + .Where(static item => item.Info is not null); + + // Compile all shaders in parallel in a single dedicated node, warming up the shared bytecode + // cache. The node produces no meaningful value: it only exists so that the join node below has + // an edge ordering it after all compilations are done (its input requires this node's output). + IncrementalValueProvider hlslBytecodeCache = + shaderInfoWithDiagnosticsInfo + .Select(static (item, _) => item.Info.HlslInfoKey) + .Collect() + .Select(static (keys, token) => + { + HlslBytecodeSyntaxProcessor.CompileAllInParallel(keys, token); + + return true; + }); + + // Join each shader with its compiled bytecode (guaranteed to be a cache hit, given the ordering edge on + // the node above), and synthesize the deferred diagnostics for the shader compilation. The resulting + // models are the same that used to be produced directly by the transform node (hence the tracking name). + IncrementalValuesProvider shaderInfo = + shaderInfoWithDiagnosticsInfo + .Combine(hlslBytecodeCache) + .Select(static (pair, token) => + { + (D2D1ShaderInfo info, HlslBytecodeDiagnosticsInfo diagnosticsInfo) = pair.Left; + + HlslBytecodeInfoKey hlslInfoKey = info.HlslInfoKey; + + // Get the compiled shader from the warmed up cache + HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token); + + token.ThrowIfCancellationRequested(); + + using ImmutableArrayBuilder diagnostics = new(); + + diagnostics.AddRange(info.Diagnostcs.AsSpan()); + + // Append any diagnostic for the shader compilation + HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(diagnosticsInfo, hlslInfo, diagnostics); + HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(diagnosticsInfo, hlslInfo, diagnostics); + + token.ThrowIfCancellationRequested(); + + return info with + { + HlslInfoKey = hlslInfoKey, + HlslInfo = hlslInfo, + Diagnostcs = diagnostics.ToImmutable() + }; + }) + .WithTrackingName(WellKnownTrackingNames.Execute); // We need to create two more incremental steps to ensure we correctly emit diagnostics and re-generate sources. // First, select an incremental provider with just the diagnostics, which will trigger every time any of them changes. diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/ComputeSharp.SourceGeneration.Hlsl.projitems b/src/ComputeSharp.SourceGeneration.Hlsl/ComputeSharp.SourceGeneration.Hlsl.projitems index abd0cb562..e71d4eab6 100644 --- a/src/ComputeSharp.SourceGeneration.Hlsl/ComputeSharp.SourceGeneration.Hlsl.projitems +++ b/src/ComputeSharp.SourceGeneration.Hlsl/ComputeSharp.SourceGeneration.Hlsl.projitems @@ -26,6 +26,7 @@ + diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs b/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs new file mode 100644 index 000000000..6d8b4d827 --- /dev/null +++ b/src/ComputeSharp.SourceGeneration.Hlsl/Models/HlslBytecodeDiagnosticsInfo.cs @@ -0,0 +1,20 @@ +using Microsoft.CodeAnalysis; + +namespace ComputeSharp.SourceGeneration.Models; + +/// +/// A model capturing the info needed to synthesize diagnostics for compiled HLSL bytecode. This makes it possible +/// to create such diagnostics after the transform node has completed (symbols cannot be used past that point), which +/// in turn allows deferring the bytecode compilation (so it can be parallelized). Note that this model holds source +/// locations (which reference syntax trees), so it is deliberately kept separate from the shader models: it only +/// travels alongside them up to the node that creates the diagnostics, and it is dropped from that point onwards. +/// +/// The fully qualified name of the shader type. +/// The location of the shader type, if available. +/// Whether the shader type is annotated to require double precision support. +/// The location of the attribute requiring double precision support, if available. +internal sealed record HlslBytecodeDiagnosticsInfo( + string TypeName, + Location? TypeLocation, + bool HasRequiresDoublePrecisionSupportAttribute, + Location? RequiresDoublePrecisionSupportAttributeLocation); diff --git a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs index f530496ee..91c2d175e 100644 --- a/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs +++ b/src/ComputeSharp.SourceGeneration.Hlsl/SyntaxProcessors/HlslBytecodeSyntaxProcessor.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Immutable; using System.ComponentModel; +using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; using System.Threading; +using System.Threading.Tasks; using ComputeSharp.SourceGeneration.Extensions; using ComputeSharp.SourceGeneration.Helpers; using ComputeSharp.SourceGeneration.Models; @@ -89,13 +92,89 @@ static unsafe HlslBytecodeInfo GetInfo(HlslBytecodeInfoKey key, CancellationToke } /// - /// Gets any diagnostics from a processed instance. + /// Compiles the shaders for all input keys in parallel, warming up the shared cache. + /// After this call, calls for any of the input keys will be cache hits. + /// + /// The instances for the shaders to compile. + /// The used to cancel the operation, if needed. + public static void CompileAllInParallel(ImmutableArray keys, CancellationToken token) + { + static void Compile(HlslBytecodeInfoKey key, CancellationToken token) + { + _ = GetInfo(ref key, token); + } + + // Skip the parallel dispatch entirely if there are less than two keys to process + if (keys.Length == 0) + { + return; + } + + if (keys.Length == 1) + { + Compile(keys[0], token); + + return; + } + + try + { + // Compile all shaders in parallel (each compilation is independent, and both the shared cache and + // the native compilers support concurrent use). Duplicate keys are filtered out first: concurrent + // requests for the same key would be benign (one result would just be discarded), but there is no + // reason to schedule them at all. The order of compilations does not matter, as the results are + // only published to the cache here (callers will then retrieve them via cache hits afterwards). + _ = Parallel.ForEach( + keys.Distinct(), + new ParallelOptions { CancellationToken = token }, + key => Compile(key, token)); + } + catch (AggregateException e) + { + // If cancellation is requested, normalize to an OperationCanceledException for the incremental + // driver (a cancellation from the callbacks may be wrapped, depending on interleaving). Other + // exceptions cannot really occur, as the compilation callback catches all expected exceptions. + token.ThrowIfCancellationRequested(); + + // If there is a single unexpected exception, unwrap it and rethrow it (preserving its stack + // trace), so that the failure reported by the compiler names the actual exception type. + if (e.InnerExceptions.Count == 1) + { + ExceptionDispatchInfo.Capture(e.InnerExceptions[0]).Throw(); + } + + throw; + } + } + + /// + /// Gets the instance for a given shader type. + /// This captures all info needed to synthesize compile diagnostics after the transform + /// node has completed (which is required, as symbols cannot be used past that point). /// /// The input instance to process. + /// The instance for the current shader. + public static HlslBytecodeDiagnosticsInfo GetDiagnosticsInfo(INamedTypeSymbol structDeclarationSymbol) + { + bool hasRequiresDoublePrecisionSupportAttribute = structDeclarationSymbol.TryGetAttributeWithFullyQualifiedMetadataName( + GetRequiresDoublePrecisionSupportAttributeName(), + out AttributeData? attributeData); + + return new HlslBytecodeDiagnosticsInfo( + TypeName: structDeclarationSymbol.ToDisplayString(), + TypeLocation: structDeclarationSymbol.Locations.FirstOrDefault(), + HasRequiresDoublePrecisionSupportAttribute: hasRequiresDoublePrecisionSupportAttribute, + RequiresDoublePrecisionSupportAttributeLocation: attributeData?.GetLocation()); + } + + /// + /// Gets any diagnostics from a processed instance. + /// + /// The instance for the current shader. /// The source instance. /// The collection of produced instances. public static void GetInfoDiagnostics( - INamedTypeSymbol structDeclarationSymbol, + HlslBytecodeDiagnosticsInfo diagnosticsInfo, HlslBytecodeInfo info, ImmutableArrayBuilder diagnostics) { @@ -105,8 +184,8 @@ public static void GetInfoDiagnostics( { diagnostic = DiagnosticInfo.Create( HlslBytecodeFailedWithWin32Exception, - structDeclarationSymbol, - structDeclarationSymbol, + diagnosticsInfo.TypeLocation, + diagnosticsInfo.TypeName, win32Error.HResult, win32Error.Message); } @@ -114,8 +193,8 @@ public static void GetInfoDiagnostics( { diagnostic = DiagnosticInfo.Create( HlslBytecodeFailedWithCompilationException, - structDeclarationSymbol, - structDeclarationSymbol, + diagnosticsInfo.TypeLocation, + diagnosticsInfo.TypeName, fxcError.Message); } @@ -128,11 +207,11 @@ public static void GetInfoDiagnostics( /// /// Gets the diagnostics for when double precision support is configured incorrectly. /// - /// The input instance to process. + /// The instance for the current shader. /// The source instance. /// The collection of produced instances. public static void GetDoublePrecisionSupportDiagnostics( - INamedTypeSymbol structDeclarationSymbol, + HlslBytecodeDiagnosticsInfo diagnosticsInfo, HlslBytecodeInfo info, ImmutableArrayBuilder diagnostics) { @@ -142,26 +221,22 @@ public static void GetDoublePrecisionSupportDiagnostics( return; } - bool hasRequiresDoublePrecisionSupportAttribute = structDeclarationSymbol.TryGetAttributeWithFullyQualifiedMetadataName( - GetRequiresDoublePrecisionSupportAttributeName(), - out AttributeData? attributeData); - // Check the two cases where diagnostics are necessary: // - The shader does not have [[D2D]RequiresDoublePrecisionSupport], but it needs it // - The shader has [[D2D]RequiresDoublePrecisionSupport], but it does not need it - if (!hasRequiresDoublePrecisionSupportAttribute && success.RequiresDoublePrecisionSupport) + if (!diagnosticsInfo.HasRequiresDoublePrecisionSupportAttribute && success.RequiresDoublePrecisionSupport) { diagnostics.Add(DiagnosticInfo.Create( MissingRequiresDoublePrecisionSupportAttribute, - structDeclarationSymbol, - structDeclarationSymbol)); + diagnosticsInfo.TypeLocation, + diagnosticsInfo.TypeName)); } - else if (hasRequiresDoublePrecisionSupportAttribute && !success.RequiresDoublePrecisionSupport) + else if (diagnosticsInfo.HasRequiresDoublePrecisionSupportAttribute && !success.RequiresDoublePrecisionSupport) { diagnostics.Add(DiagnosticInfo.Create( UnnecessaryRequiresDoublePrecisionSupportAttribute, - attributeData!.GetLocation(), - structDeclarationSymbol)); + diagnosticsInfo.RequiresDoublePrecisionSupportAttributeLocation, + diagnosticsInfo.TypeName)); } } diff --git a/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs b/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs index 1ed999402..c614ac592 100644 --- a/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs +++ b/src/ComputeSharp.SourceGenerators/ComputeShaderDescriptorGenerator.cs @@ -26,8 +26,9 @@ public sealed partial class ComputeShaderDescriptorGenerator : IIncrementalGener /// public void Initialize(IncrementalGeneratorInitializationContext context) { - // Discover all shader types and extract all the necessary info from each of them - IncrementalValuesProvider shaderInfo = + // Discover all shader types and extract all the necessary info from each of them (with the exception + // of the compiled HLSL bytecode, which is processed later). See more notes in the D2D1 generator. + IncrementalValuesProvider<(ShaderInfo Info, HlslBytecodeDiagnosticsInfo DiagnosticsInfo)> shaderInfoWithDiagnosticsInfo = context.SyntaxProvider .ForAttributeWithMetadataName( "ComputeSharp.GeneratedComputeShaderDescriptorAttribute", @@ -117,16 +118,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - // Prepare the lookup key for the HLSL shader bytecode + // Prepare the lookup key for the HLSL shader bytecode. The compilation is deliberately + // deferred to a dedicated node below, so all shaders can be compiled in parallel there + // (the incremental driver invokes transform callbacks sequentially, so compiling here + // would serialize all shader compilations). HlslBytecodeInfoKey hlslInfoKey = new(hlslSource, compileOptions, isCompilationEnabled); - // Try to get the HLSL bytecode - HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token); - - token.ThrowIfCancellationRequested(); - - HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(typeSymbol, hlslInfo, diagnostics); - HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(typeSymbol, hlslInfo, diagnostics); + // Capture the info needed to synthesize the diagnostics for the deferred compilation, + // as they cannot be created later (symbols must not be used past the transform node) + HlslBytecodeDiagnosticsInfo hlslDiagnosticsInfo = HlslBytecodeSyntaxProcessor.GetDiagnosticsInfo(typeSymbol); token.ThrowIfCancellationRequested(); @@ -135,7 +135,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - return new ShaderInfo( + ShaderInfo info = new( Hierarchy: hierarchyInfo, ThreadsX: threadsX, ThreadsY: threadsY, @@ -147,11 +147,61 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Resources: resourceInfo, ResourceDescriptors: resourceDescriptors, HlslInfoKey: hlslInfoKey, - HlslInfo: hlslInfo, + HlslInfo: HlslBytecodeInfo.Missing.Instance, Diagnostcs: diagnostics.ToImmutable()); + + return (Info: info, DiagnosticsInfo: hlslDiagnosticsInfo); }) - .WithTrackingName(WellKnownTrackingNames.Execute) - .Where(static item => item is not null)!; + .Where(static item => item.Info is not null); + + // Compile all shaders in parallel in a single dedicated node, warming up the shared bytecode + // cache (see more notes in the D2D1 generator). The node produces no meaningful value: it only + // exists so that the join node below has an edge ordering it after all compilations are done. + IncrementalValueProvider hlslBytecodeCache = + shaderInfoWithDiagnosticsInfo + .Select(static (item, _) => item.Info.HlslInfoKey) + .Collect() + .Select(static (keys, token) => + { + HlslBytecodeSyntaxProcessor.CompileAllInParallel(keys, token); + + return true; + }); + + // Join each shader with its compiled bytecode (guaranteed to be a cache hit, given the ordering + // edge on the node above), and synthesize the deferred diagnostics for the shader compilation + IncrementalValuesProvider shaderInfo = + shaderInfoWithDiagnosticsInfo + .Combine(hlslBytecodeCache) + .Select(static (pair, token) => + { + (ShaderInfo info, HlslBytecodeDiagnosticsInfo diagnosticsInfo) = pair.Left; + + HlslBytecodeInfoKey hlslInfoKey = info.HlslInfoKey; + + // Get the compiled shader from the warmed up cache + HlslBytecodeInfo hlslInfo = HlslBytecodeSyntaxProcessor.GetInfo(ref hlslInfoKey, token); + + token.ThrowIfCancellationRequested(); + + using ImmutableArrayBuilder diagnostics = new(); + + diagnostics.AddRange(info.Diagnostcs.AsSpan()); + + // Append any diagnostic for the shader compilation + HlslBytecodeSyntaxProcessor.GetInfoDiagnostics(diagnosticsInfo, hlslInfo, diagnostics); + HlslBytecodeSyntaxProcessor.GetDoublePrecisionSupportDiagnostics(diagnosticsInfo, hlslInfo, diagnostics); + + token.ThrowIfCancellationRequested(); + + return info with + { + HlslInfoKey = hlslInfoKey, + HlslInfo = hlslInfo, + Diagnostcs = diagnostics.ToImmutable() + }; + }) + .WithTrackingName(WellKnownTrackingNames.Execute); // Split the diagnostics, and drop them from the output provider (see more notes in the D2D1 generator) IncrementalValuesProvider> diagnosticInfo = diff --git a/tests/ComputeSharp.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs b/tests/ComputeSharp.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs index caf81758f..8a9c3ef90 100644 --- a/tests/ComputeSharp.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs +++ b/tests/ComputeSharp.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs @@ -31,7 +31,7 @@ public float4 Execute() } """; - CSharpGeneratorTest.VerifyDiagnostics(source, "CMPSD2D0080"); + CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPSD2D0080", "MyShader")); } [TestMethod] @@ -59,6 +59,49 @@ public float4 Execute() } """; - CSharpGeneratorTest.VerifyDiagnostics(source, "CMPSD2D0081"); + CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPSD2D0081", "D2DRequiresDoublePrecisionSupport")); + } + + [TestMethod] + public void HlslBytecodeFailedWithCompilationException() + { + const string source = """ + using ComputeSharp; + using ComputeSharp.D2D1; + using float4 = global::ComputeSharp.Float4; + + namespace MyNamespace; + + internal static class ClassWithRecursiveMembers + { + public static int A(int x) + { + if (x <= 1) + { + return x; + } + + return A(x - 1); + } + } + + [D2DInputCount(0)] + [D2DShaderProfile(D2D1ShaderProfile.PixelShader50)] + [D2DGeneratedPixelShaderDescriptor] + internal readonly partial struct MyShader : ID2D1PixelShader + { + private readonly int value; + + public float4 Execute() + { + return ClassWithRecursiveMembers.A(value); + } + } + """; + + // The HLSL compilation is expected to fail because recursion is not supported. This also + // verifies that the diagnostic is reported on the shader type, even though the compilation + // is deferred to a separate incremental node (ie. it's no longer done in the transform node). + CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPSD2D0034", "MyShader")); } } \ No newline at end of file diff --git a/tests/ComputeSharp.Tests.SourceGenerators/Helpers/CSharpGeneratorTest{TGenerator}.cs b/tests/ComputeSharp.Tests.SourceGenerators/Helpers/CSharpGeneratorTest{TGenerator}.cs index 08432e47d..efb85189a 100644 --- a/tests/ComputeSharp.Tests.SourceGenerators/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/tests/ComputeSharp.Tests.SourceGenerators/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -17,6 +17,7 @@ using Basic.Reference.Assemblies; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ComputeSharp.Tests.SourceGenerators.Helpers; @@ -52,6 +53,35 @@ public static void VerifyDiagnostics(string source, params string[] diagnosticsI } } + /// + /// Verifies the resulting diagnostics from a source generator, including their locations. + /// + /// The input source to process. + /// The expected diagnostics, as pairs of ids and the (unique) text in they should be reported on. + public static void VerifyDiagnostics(string source, params (string Id, string LocationText)[] diagnostics) + { + RunGenerator(source, out _, out ImmutableArray actualDiagnostics); + + Dictionary diagnosticMap = actualDiagnostics.DistinctBy(diagnostic => diagnostic.Id).ToDictionary(diagnostic => diagnostic.Id); + + // Check that the diagnostics match + Assert.IsTrue(diagnosticMap.Keys.ToHashSet().SetEquals(diagnostics.Select(diagnostic => diagnostic.Id)), $"Diagnostics didn't match. {string.Join(", ", diagnosticMap.Values)}"); + + // Check that each diagnostic has the expected source location + foreach ((string id, string locationText) in diagnostics) + { + int index = source.IndexOf(locationText, StringComparison.Ordinal); + + Assert.IsTrue(index >= 0, $"The text '{locationText}' was not found in the source."); + Assert.IsTrue(source.IndexOf(locationText, index + 1, StringComparison.Ordinal) < 0, $"The text '{locationText}' must appear exactly once in the source."); + + Location location = diagnosticMap[id].Location; + + Assert.IsTrue(location.IsInSource, $"Diagnostic '{id}' has no source location (kind: {location.Kind})."); + Assert.AreEqual(new TextSpan(index, locationText.Length), location.SourceSpan, $"Diagnostic '{id}' has an unexpected location."); + } + } + /// /// Verifies the resulting sources produced by a source generator. /// diff --git a/tests/ComputeSharp.Tests.SourceGenerators/Test_ComputeShaderDescriptorGenerator_Diagnostics.cs b/tests/ComputeSharp.Tests.SourceGenerators/Test_ComputeShaderDescriptorGenerator_Diagnostics.cs index 2f4a468d0..27ab278c8 100644 --- a/tests/ComputeSharp.Tests.SourceGenerators/Test_ComputeShaderDescriptorGenerator_Diagnostics.cs +++ b/tests/ComputeSharp.Tests.SourceGenerators/Test_ComputeShaderDescriptorGenerator_Diagnostics.cs @@ -1498,7 +1498,7 @@ public void Execute() // The HLSL compilation is expected to fail because recursion is not actually supported: // // "error: recursive functions not allowed" - CSharpGeneratorTest.VerifyDiagnostics(source, "CMPS0046"); + CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPS0046", "ShaderWithRecursiveMembers")); } [TestMethod] @@ -1523,7 +1523,7 @@ public void Execute() } """; - CSharpGeneratorTest.VerifyDiagnostics(source, "CMPS0064"); + CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPS0064", "MyShader")); } [TestMethod] @@ -1549,6 +1549,6 @@ public void Execute() } """; - CSharpGeneratorTest.VerifyDiagnostics(source, "CMPS0065"); + CSharpGeneratorTest.VerifyDiagnostics(source, ("CMPS0065", "RequiresDoublePrecisionSupport")); } } \ No newline at end of file