Skip to content

Commit

Permalink
Merge pull request #877 from Sergio0694/dev/d2d-intrinsic-constant-an…
Browse files Browse the repository at this point in the history
…alyzer

Add analyzer for non constant input arguments for all 'D2D' intrinsics
  • Loading branch information
Sergio0694 authored Nov 26, 2024
2 parents 132cfab + 82ab453 commit 0fbe1a6
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,5 @@ CMPSD2D0081 | ComputeSharp.D2D1.Shaders | Warning | [Documentation](https://gith
CMPSD2D0082 | ComputeSharp.D2D1.Shaders | Warning | [Documentation](https://github.com/Sergio0694/ComputeSharp)
CMPSD2D0083 | ComputeSharp.D2D1.Shaders | Error | [Documentation](https://github.com/Sergio0694/ComputeSharp)
CMPSD2D0084 | ComputeSharp.D2D1.Shaders | Error | [Documentation](https://github.com/Sergio0694/ComputeSharp)
CMPSD2D0085 | ComputeSharp.D2D1.Shaders | Error | [Documentation](https://github.com/Sergio0694/ComputeSharp)
CMPSD2D0086 | ComputeSharp.D2D1.Shaders | Error | [Documentation](https://github.com/Sergio0694/ComputeSharp)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using ComputeSharp.D2D1.Intrinsics;
using ComputeSharp.SourceGeneration.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using static ComputeSharp.SourceGeneration.Diagnostics.DiagnosticDescriptors;
Expand All @@ -21,7 +22,8 @@ public sealed class InvalidD2DInputArgumentAnalyzer : DiagnosticAnalyzer
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
[
IndexOutOfRangeForD2DIntrinsic,
InvalidInputTypeForD2DIntrinsic
InvalidInputTypeForD2DIntrinsic,
InvalidIndexSyntaxForD2DIntrinsic
];

/// <inheritdoc/>
Expand Down Expand Up @@ -59,7 +61,13 @@ public override void Initialize(AnalysisContext context)
// Second cheap inital filter: we only care about invocations with a constant 'int' argument in first position.
// While we're validating this, let's also get the 'index' parameter, since we need to validate it anyway.
if (operation.Arguments is not [{ Value.ConstantValue: { HasValue: true, Value: int index } }, ..])
if (operation.Arguments is not [{ Value.ConstantValue: { HasValue: true, Value: int index } } firstArgument, ..])
{
return;
}
// We only want to kick in when the target parameter is an 'int' (same as in 'NonConstantD2DInputArgumentAnalyzer')
if (firstArgument.Parameter is not { Type.SpecialType: SpecialType.System_Int32 } parameterSymbol)
{
return;
}
Expand All @@ -70,6 +78,27 @@ public override void Initialize(AnalysisContext context)
return;
}
// Also check that the actual syntax for the index argument is valid (only literals and direct constant field references
// are valid). For parenthesized expressions, we need to check the parent syntax node (the operation just ignores it).
if (firstArgument.Value is not (ILiteralOperation or IFieldReferenceOperation))
{
context.ReportDiagnostic(Diagnostic.Create(
InvalidIndexSyntaxForD2DIntrinsic,
firstArgument.Syntax.GetLocation(),
parameterSymbol.Name,
targetMethodSymbol.Name));
}
else if (firstArgument.Syntax.Parent.IsKind(SyntaxKind.ParenthesizedExpression))
{
// In this case, we pass the parent syntax node for the location, so that we can
// include the parentheses as well. Otherwise we'd just underline the inner value.
context.ReportDiagnostic(Diagnostic.Create(
InvalidIndexSyntaxForD2DIntrinsic,
firstArgument.Syntax.Parent.GetLocation(),
parameterSymbol.Name,
targetMethodSymbol.Name));
}
// We have matched a target symbol, so let's try to get the parent shader
if (context.ContainingSymbol.FirstAncestorOrSelf<INamedTypeSymbol>() is not { TypeKind: TypeKind.Struct } typeSymbol)
{
Expand All @@ -95,7 +124,7 @@ public override void Initialize(AnalysisContext context)
{
context.ReportDiagnostic(Diagnostic.Create(
IndexOutOfRangeForD2DIntrinsic,
operation.Syntax.GetLocation(),
firstArgument.Syntax.GetLocation(),
targetMethodSymbol.Name,
index,
typeSymbol,
Expand All @@ -106,7 +135,7 @@ public override void Initialize(AnalysisContext context)
// Second validation: the input type must match
context.ReportDiagnostic(Diagnostic.Create(
InvalidInputTypeForD2DIntrinsic,
operation.Syntax.GetLocation(),
firstArgument.Syntax.GetLocation(),
targetMethodSymbol.Name,
index));
}
Expand All @@ -130,23 +159,23 @@ private static bool TryBuildMethodSymbolMap(Compilation compilation, [NotNullWhe
return false;
}

// Get the symbols for all relevant 'D2D' methods
IMethodSymbol?[] d2DMethodSymbols =
// These are all the 'D2D' intrinsic methods that take a target shader input index
string[] d2DMethodNames =
[
d2DSymbol.GetMethod(nameof(D2D.GetInput)),
d2DSymbol.GetMethod(nameof(D2D.GetInputCoordinate)),
d2DSymbol.GetMethod(nameof(D2D.SampleInput)),
d2DSymbol.GetMethod(nameof(D2D.SampleInputAtOffset)),
d2DSymbol.GetMethod(nameof(D2D.SampleInputAtPosition))
nameof(D2D.GetInput),
nameof(D2D.GetInputCoordinate),
nameof(D2D.SampleInput),
nameof(D2D.SampleInputAtOffset),
nameof(D2D.SampleInputAtPosition)
];

ImmutableDictionary<IMethodSymbol, D2D1PixelShaderInputType?>.Builder inputTypeMethodMap = ImmutableDictionary.CreateBuilder<IMethodSymbol, D2D1PixelShaderInputType?>(SymbolEqualityComparer.Default);

// Validate all methods and build the map
foreach (IMethodSymbol? d2DMethodSymbol in d2DMethodSymbols)
foreach (string d2DMethodName in d2DMethodNames)
{
// We failed to find a symbol (shouldn't really ever happen), just stop here
if (d2DMethodSymbol is null)
// If we can't resolve a symbol (shouldn't really ever happen), just stop here
if (d2DSymbol.GetMethod(d2DMethodName) is not { } d2DMethodSymbol)
{
methodSymbols = null;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using ComputeSharp.SourceGeneration.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using static ComputeSharp.SourceGeneration.Diagnostics.DiagnosticDescriptors;

namespace ComputeSharp.D2D1.SourceGenerators;

/// <summary>
/// A diagnostic analyzer that generates an error whenever an invocation to a D2D intrinsic is using an argument that is not constant for the source shader input.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NonConstantD2DInputArgumentAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [NonConstantSourceInputIndexForD2DIntrinsic];

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(static context =>
{
// Ensure we can get all methods, or stop immediately
if (!TryBuildMethodSymbolSet(context.Compilation, out ImmutableHashSet<IMethodSymbol>? methodSymbols))
{
return;
}
context.CancellationToken.ThrowIfCancellationRequested();
// Register a callback for each argument we want to validate
context.RegisterOperationAction(context =>
{
IArgumentOperation operation = (IArgumentOperation)context.Operation;
// Ensure the argument is for an invocation, and get its first argument
if (operation.Parent is not IInvocationOperation { Arguments: [{ } firstArgument, ..] } invocationOperation)
{
return;
}
// We also want to verify that the argument is the first one (that's the only one that should be constant)
if (operation != firstArgument)
{
return;
}
// Ensure the argument is for a 'D2D' method (ignore all other arguments, ie. almost all of them)
if (invocationOperation is not { TargetMethod: { IsStatic: true, ContainingType.Name: "D2D" } targetMethodSymbol })
{
return;
}
// We only want to kick in when the target parameter is an 'int'
if (operation is not { Parameter: { Type.SpecialType: SpecialType.System_Int32 } parameterSymbol })
{
return;
}
// This analyzer should also only kick in when the argument is not constant
if (operation.Value.ConstantValue.HasValue)
{
return;
}
// Now we can actually verify that the target method is indeed one we care about (this should always match)
if (!methodSymbols.Contains(targetMethodSymbol))
{
return;
}
// Finally, we can emit the diagnostic
context.ReportDiagnostic(Diagnostic.Create(
NonConstantSourceInputIndexForD2DIntrinsic,
operation.Syntax.GetLocation(),
parameterSymbol.Name,
targetMethodSymbol.Name));
}, OperationKind.Argument);
});
}

/// <summary>
/// Tries to build a set of <see cref="IMethodSymbol"/> instances for all D2D intrinsics targeting a specific shader input.
/// </summary>
/// <param name="compilation">The <see cref="Compilation"/> to consider for analysis.</param>
/// <param name="methodSymbols">The resulting set of resolved <see cref="IMethodSymbol"/> instances.</param>
/// <returns>Whether all requested <see cref="IMethodSymbol"/> instances could be resolved.</returns>
private static bool TryBuildMethodSymbolSet(Compilation compilation, [NotNullWhen(true)] out ImmutableHashSet<IMethodSymbol>? methodSymbols)
{
// Get the 'D2D' symbol, to get methods from it
if (compilation.GetTypeByMetadataName("ComputeSharp.D2D1.D2D") is not { } d2DSymbol)
{
methodSymbols = null;

return false;
}

// All 'D2D' intrinsics targeting an input (see 'InvalidD2DInputArgumentAnalyzer')
string[] d2DMethodNames =
[
nameof(D2D.GetInput),
nameof(D2D.GetInputCoordinate),
nameof(D2D.SampleInput),
nameof(D2D.SampleInputAtOffset),
nameof(D2D.SampleInputAtPosition)
];

ImmutableHashSet<IMethodSymbol>.Builder inputTypeMethodSet = ImmutableHashSet.CreateBuilder<IMethodSymbol>(SymbolEqualityComparer.Default);

// Validate all methods and build the map
foreach (string d2DMethodName in d2DMethodNames)
{
// Ensure we can find the target method symbol (again just like in 'InvalidD2DInputArgumentAnalyzer')
if (d2DSymbol.GetMethod(d2DMethodName) is not { } d2DMethodSymbol)
{
methodSymbols = null;

return false;
}

_ = inputTypeMethodSet.Add(d2DMethodSymbol);
}

methodSymbols = inputTypeMethodSet.ToImmutable();

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1269,4 +1269,36 @@ partial class DiagnosticDescriptors
isEnabledByDefault: true,
description: "D2D intrinsics must be used with compatible input types (the input type of each shader input can be configured using [D2DInputSimple] and [D2DInputComplex] on the shader type).",
helpLinkUri: "https://github.com/Sergio0694/ComputeSharp");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for a D2D intrinsic call with a source input index that is not a constant.
/// <para>
/// Format: <c>"The argument for parameter '{0}' in the call to the D2D intrinsic '{1}' must be a constant value (for instance, 'D2D.GetInput(0)' is valid, whereas 'D2D.GetInput(x)' is not)"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor NonConstantSourceInputIndexForD2DIntrinsic = new(
id: "CMPSD2D0085",
title: "Non constant input index for D2D intrinsic",
messageFormat: "The argument for parameter '{0}' in the call to the D2D intrinsic '{1}' must be a constant value (for instance, 'D2D.GetInput(0)' is valid, whereas 'D2D.GetInput(x)' is not)",
category: "ComputeSharp.D2D1.Shaders",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Arguments for parameters indicating the source shader input index in calls to D2D intrinsics must be constant values (for instance, 'D2D.GetInput(0)' is valid, whereas 'D2D.GetInput(x)' is not).",
helpLinkUri: "https://github.com/Sergio0694/ComputeSharp");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for a D2D intrinsic call with a source input index using invalid syntax.
/// <para>
/// Format: <c>"The argument for parameter '{0}' in the call to the D2D intrinsic '{1}' must be a literal or a constant field reference (for instance, 'D2D.GetInput(0)' and 'D2D.GetInput(TextureIndex)' are valid, whereas 'D2D.GetInput(BaseIndex + 1)' and 'D2D.GetInput(1 + 2)' are not)"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor InvalidIndexSyntaxForD2DIntrinsic = new(
id: "CMPSD2D0086",
title: "Invalid input index syntax for D2D intrinsic",
messageFormat: "The argument for parameter '{0}' in the call to the D2D intrinsic '{1}' must be a literal or a constant field reference (for instance, 'D2D.GetInput(0)' and 'D2D.GetInput(TextureIndex)' are valid, whereas 'D2D.GetInput(BaseIndex + 1)' and 'D2D.GetInput(1 + 2)' are not)",
category: "ComputeSharp.D2D1.Shaders",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Arguments for parameters indicating the source shader input index in calls to D2D intrinsics must be a literal or constant field references (for instance, 'D2D.GetInput(0)' and 'D2D.GetInput(TextureIndex)' are valid, whereas 'D2D.GetInput(BaseIndex + 1)' and 'D2D.GetInput(1 + 2)' are not).",
helpLinkUri: "https://github.com/Sergio0694/ComputeSharp");
}
Loading

0 comments on commit 0fbe1a6

Please sign in to comment.