Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CONTRIBUTING.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,15 @@ private static ImmutableArray<HlslStaticField> GetStaticFields(
}
}

// An access C# performs before the field's initializer has run is reported once every initializer
// is rewritten (same as in the DX12 generator)
HlslDefinitionsSyntaxProcessor.ReportStaticFieldAccessesBeforeInitialization(
structDeclarationSymbol,
staticFieldDefinitions,
semanticModel,
diagnostics,
token);

// One sequence ordered by when each field finished (same as in the DX12 generator)
HlslStaticField[] fields = [.. declared.WrittenSpan, .. staticFieldDefinitions.Values];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1447,19 +1447,19 @@ partial class DiagnosticDescriptors
helpLinkUri: "https://github.com/routersys/ComputeWeave");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> for a static field initializer reaching the field it initializes.
/// Gets a <see cref="DiagnosticDescriptor"/> for a static field accessed before its initializer has run.
/// <para>
/// Format: <c>"The static field {0} is initialized from an expression that reads it back, directly or through a declaration that initializer reaches"</c>.
/// Format: <c>"The static field {0} is accessed while the initializer of {1} is running, before its own initializer has run (C# reads the default value of its type there and discards a write, which the generated HLSL does not reproduce)"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor CyclicStaticFieldInitializer = new(
public static readonly DiagnosticDescriptor StaticFieldAccessedBeforeInitialization = new(
id: "CMPWD2D0096",
title: "Static field initializer reaching the field it initializes",
messageFormat: "The static field {0} is initialized from an expression that reads it back, directly or through a declaration that initializer reaches",
title: "Static field accessed before its initializer has run",
messageFormat: "The static field {0} is accessed while the initializer of {1} is running, before its own initializer has run (C# reads the default value of its type there and discards a write, which the generated HLSL does not reproduce)",
category: "ComputeWeave.D2D1.Shaders",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "C# runs the initializer once, and where the cycle closes it reads the field as its default value, so the field holds whatever that produces. HLSL has no defined order for global static initializers, so the value the shader computes is not the one C# computes, and the shader compiler accepts the source without saying anything: the generated HLSL was measured to compile with the cycle written out as it stands. Before this report the generator faulted instead, adding the same key to the collection of static field definitions twice, which discards the descriptors for every shader in the compilation unit and leaves the author with errors that name none of this.",
description: "C# runs the static field initializers of a type once, in the order the fields are declared. A field whose turn has not come yet holds the default value of its type: a read of it is that default, and a write to it is discarded when its initializer runs. Both happen when an initializer reaches a field of the same type declared after it, directly or through any declaration it reaches, and the read happens when an initializer reaches back into the field it initializes. The generated HLSL reproduces neither: the shader compiler folds the initializer of a constant declared later, so the read is the initialized value, and refuses the other forms, so the shader either computes a value C# never produces or fails against generated code the author never wrote. A field carrying no initializer holds the default value in both and is not reported, and neither is a write to the field being initialized, which its initializer overwrites in both. Before this report an initializer reaching itself faulted the generator instead, adding the same key to the collection of static field definitions twice, which discards the descriptors for every shader in the compilation unit and leaves the author with errors that name none of this.",
helpLinkUri: "https://github.com/routersys/ComputeWeave");

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using static ComputeWeave.SourceGeneration.Diagnostics.DiagnosticDescriptors;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

Expand Down Expand Up @@ -117,19 +118,6 @@ public static bool TryGetStaticField(

token.ThrowIfCancellationRequested();

// A field of the shader is claimed before its initializer is rewritten, the way the caller that
// rewrites an external one already claims its entry, so that a read of the field reaching back into
// its own initializer finds a claim rather than an identifier written out as it stands. The claim is
// released below, this collection being the external definitions a generator writes out on top of
// the fields of the shader it gathers itself, which this field is already one of. The order a
// completed entry carries is never read for this one, the claim being released before that
bool isShaderStaticField = SymbolEqualityComparer.Default.Equals(fieldSymbol.ContainingType, structDeclarationSymbol);

if (isShaderStaticField)
{
staticFieldDefinitions.Add(fieldSymbol, (name, null, null, 0));
}

// Create the rewriter to use, which is also returned to callers so they can extract the local
// functions an initializer lifted out. What the initializer requires of the shader is raised
// into the shared requirements instead, so a caller has nothing to read back out for that.
Expand All @@ -150,11 +138,6 @@ public static bool TryGetStaticField(

token.ThrowIfCancellationRequested();

if (isShaderStaticField)
{
_ = staticFieldDefinitions.Remove(fieldSymbol);
}

assignmentExpression = processedDeclaration?.NormalizeWhitespace(eol: "\n").ToFullString();

return true;
Expand Down Expand Up @@ -356,4 +339,177 @@ public static int GetStaticFieldOrder(IDictionary<IFieldSymbol, HlslStaticField>

return order;
}

/// <summary>
/// Reports every access to a static field that C# performs before the initializer of that field has run.
/// </summary>
/// <param name="structDeclarationSymbol">The type symbol for the shader type.</param>
/// <param name="staticFieldDefinitions">The collection of discovered static field definitions.</param>
/// <param name="semanticModel">The <see cref="SemanticModelProvider"/> instance for the type to process.</param>
/// <param name="diagnostics">The collection of produced <see cref="DiagnosticInfo"/> instances.</param>
/// <param name="token">The <see cref="CancellationToken"/> used to cancel the operation, if needed.</param>
/// <remarks>
/// <para>
/// C# runs the static field initializers of a type once, in the order the fields are declared. A field of
/// that type whose initializer has not run yet holds the default value of its type, so a read of it from the
/// initializer being run, or from anything that initializer reaches, is that default, and a write to it is
/// discarded when its initializer runs. The generated HLSL reproduces neither: the shader compiler folds
/// the initializer the field carries when it can, so the read is the initialized value and the write stays,
/// leaves the read undefined otherwise, and does not compile a direct read of a global declared later. The
/// field being initialized is the first one whose turn has not come, so an initializer reaching back into
/// itself is the same case, except for a write, which the initializer overwrites in the generated HLSL as
/// well. A field carrying no initializer holds the default value in both and is not part of this.
/// </para>
/// <para>
/// The walk starts from the initializer of every static field the generated HLSL declares, the ones of the
/// shader and the imported ones alike, and follows every declaration it reaches: the method, local function
/// or constructor a call resolves to, and the initializer of a static field of another type, whose
/// initializers C# runs when that type is first touched. A field of the same type is not walked into, its
/// initializer having run already or not running until its own turn, and a local function is walked only
/// through a call, C# not running one that is never called. An access two initializers both perform too
/// early is one place to change, so it is reported once, for the first of the two in declaration order.
/// The walk does not follow the order of the statements it passes, so a read after a write in the same
/// declaration is reported like any other.
/// </para>
/// <para>
/// This runs once after the initializers are rewritten rather than as they are, because a rewriting does
/// not pass through every declaration an initializer reaches: a declaration is imported once, so one the
/// body or an earlier initializer imported is not rewritten again, and a method of the shader is never
/// imported. Semantic information is resolved only for the kinds an access, a call or a construction can
/// be written as, an access written as a member access resolving on the access alone.
/// </para>
/// </remarks>
public static void ReportStaticFieldAccessesBeforeInitialization(
INamedTypeSymbol structDeclarationSymbol,
IDictionary<IFieldSymbol, HlslStaticField> staticFieldDefinitions,
SemanticModelProvider semanticModel,
ImmutableArrayBuilder<DiagnosticInfo> diagnostics,
CancellationToken token)
{
Dictionary<INamedTypeSymbol, List<(IFieldSymbol Field, ExpressionSyntax Initializer)>> initializers = new(SymbolEqualityComparer.Default);

// An access is one place the author has to change, so one reached from two initializers is reported
// once, naming the first of the two in declaration order
HashSet<SyntaxNode> reportedAccesses = [];

// The static fields of a type that carry an initializer, in the order C# runs those
List<(IFieldSymbol Field, ExpressionSyntax Initializer)> GetInitializers(INamedTypeSymbol type)
{
if (!initializers.TryGetValue(type, out List<(IFieldSymbol Field, ExpressionSyntax Initializer)>? order))
{
order = [];

foreach (ISymbol member in type.GetMembers())
{
if (member is IFieldSymbol { IsImplicitlyDeclared: false, IsStatic: true, IsConst: false } field &&
field.TryGetSyntaxNode(token, out VariableDeclaratorSyntax? declarator) &&
declarator.Initializer is { } initializer)
{
order.Add((field, initializer.Value));
}
}

initializers.Add(type, order);
}

return order;
}

// A simple assignment to the field and an out argument write it without reading it
static bool IsWrite(IFieldReferenceOperation reference)
{
return reference.Parent switch
{
ISimpleAssignmentOperation assignment => ReferenceEquals(assignment.Target, reference),
IArgumentOperation { Parameter.RefKind: RefKind.Out } => true,
_ => false
};
}

// The imported fields are walked in the declaration order of their type too, so that an access two of
// them perform too early names the first of the two
HashSet<IFieldSymbol> importedFields = new(staticFieldDefinitions.Keys, SymbolEqualityComparer.Default);
HashSet<INamedTypeSymbol> importedTypes = new(SymbolEqualityComparer.Default);
List<(IFieldSymbol Field, ExpressionSyntax Initializer)> roots = [.. GetInitializers(structDeclarationSymbol)];

foreach (IFieldSymbol importedField in staticFieldDefinitions.Keys)
{
if (importedTypes.Add(importedField.ContainingType))
{
roots.AddRange(GetInitializers(importedField.ContainingType).Where(root => importedFields.Contains(root.Field)));
}
}

foreach ((IFieldSymbol field, ExpressionSyntax initializer) in roots)
{
token.ThrowIfCancellationRequested();

// The fields of the type whose initializer has not run when this one runs: this field and the ones after it
HashSet<IFieldSymbol> pending = new(
GetInitializers(field.ContainingType).SkipWhile(candidate => !SymbolEqualityComparer.Default.Equals(candidate.Field, field)).Select(candidate => candidate.Field),
SymbolEqualityComparer.Default);
HashSet<ISymbol> visited = new(SymbolEqualityComparer.Default);

WalkReachedNodes(initializer);

void WalkReachedDeclaration(ISymbol symbol, SyntaxNode? root)
{
if (root is not null && visited.Add(symbol))
{
WalkReachedNodes(root);
}
}

void WalkReachedNodes(SyntaxNode root)
{
// The initializer can be the access or the call itself, so the root counts as a reached node too.
// A local function is entered from a call to it, the way a method is, rather than from the body
// it is declared in, that body not running it unless it calls it
foreach (SyntaxNode node in root.DescendantNodesAndSelf(descendant => descendant is not LocalFunctionStatementSyntax || ReferenceEquals(descendant, root)))
{
token.ThrowIfCancellationRequested();

if (node is not
(IdentifierNameSyntax or
MemberAccessExpressionSyntax or
InvocationExpressionSyntax or
BaseObjectCreationExpressionSyntax))
{
continue;
}

switch (semanticModel.For(node).GetOperation(node, token))
{
case IFieldReferenceOperation { Field: { IsStatic: true, IsConst: false } reachedField } reference:
if (pending.Contains(reachedField))
{
// A write to the field being initialized is overwritten by its initializer in
// the generated HLSL as well, so it is the one access that is not reported
if ((!IsWrite(reference) || !SymbolEqualityComparer.Default.Equals(reachedField, field)) &&
reportedAccesses.Add(node))
{
diagnostics.Add(StaticFieldAccessedBeforeInitialization, node, reachedField, field);
}
}
else if (!SymbolEqualityComparer.Default.Equals(reachedField.ContainingType, field.ContainingType) &&
reachedField.TryGetSyntaxNode(token, out VariableDeclaratorSyntax? reachedDeclarator))
{
WalkReachedDeclaration(reachedField, reachedDeclarator.Initializer?.Value);
}

break;
case IInvocationOperation { TargetMethod: { MethodKind: MethodKind.LocalFunction } localFunction }:
WalkReachedDeclaration(localFunction, localFunction.TryGetSyntaxNode(token, out LocalFunctionStatementSyntax? localFunctionStatement) ? localFunctionStatement : null);
break;
case IInvocationOperation { TargetMethod: { } method }:
WalkReachedDeclaration(method, method.TryGetSyntaxNode(token, out MethodDeclarationSyntax? methodDeclaration) ? methodDeclaration : null);
break;
case IObjectCreationOperation { Constructor: { } constructor }:
WalkReachedDeclaration(constructor, constructor.TryGetSyntaxNode(token, out ConstructorDeclarationSyntax? constructorDeclaration) ? constructorDeclaration : null);
break;
}
}
}
}
}
}
Loading
Loading