From 2fb7f200f4accec21f05b039ad48ac6bbfdbb028 Mon Sep 17 00:00:00 2001 From: routersys Date: Thu, 17 Sep 2026 10:17:12 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E5=B1=80=E6=89=80=E5=A4=89=E6=95=B0?= =?UTF-8?q?=E3=82=84=E5=BC=95=E6=95=B0=E3=81=AB=E9=9A=A0=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E3=82=B7=E3=82=A7=E3=83=BC=E3=83=80=E3=83=BC=E3=81=AE?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=90=E3=83=BC=E3=81=AE=E8=AA=AD=E3=81=BF?= =?UTF-8?q?=E3=82=92=E8=A8=BA=E6=96=AD=E3=81=A7=E6=96=AD=E3=81=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.ja.md | 4 +- README.md | 4 +- .../AnalyzerReleases.Unshipped.md | 1 + .../Diagnostics/DiagnosticDescriptors.cs | 20 +++++++ .../HlslSourceRewriter.Diagnostics.cs | 56 +++++++++++++++++++ .../SyntaxRewriters/ShaderSourceRewriter.cs | 6 ++ .../SyntaxRewriters/StaticFieldRewriter.cs | 1 + .../AnalyzerReleases.Unshipped.md | 1 + .../Diagnostics/DiagnosticDescriptors.cs | 16 ++++++ src/ComputeWeave/README.ja.md | 2 +- src/ComputeWeave/README.md | 2 +- 11 files changed, 107 insertions(+), 6 deletions(-) diff --git a/README.ja.md b/README.ja.md index 0de01696b..fd7e23468 100644 --- a/README.ja.md +++ b/README.ja.md @@ -260,7 +260,7 @@ GPUが書いたバッファを、以降のシェーダーへ読み取り専用 ### コンパイル時の検証 -以上の宣言はアナライザーが検査し、接頭辞 `CMPW` の診断113種類として報告します。対象は属性の位置、ホストとパイプラインメソッドの形、スロットの宣言、資源の契約、生成されるオーバーロードの衝突です。一部にはコード修正が付きます。 +以上の宣言はアナライザーが検査し、接頭辞 `CMPW` の診断114種類として報告します。対象は属性の位置、ホストとパイプラインメソッドの形、スロットの宣言、資源の契約、生成されるオーバーロードの衝突です。一部にはコード修正が付きます。 --- @@ -297,7 +297,7 @@ ReadOnlyMemory bytecode = D2D1PixelShader.LoadBytecode() ReadOnlyMemory buffer = D2D1PixelShader.GetConstantBuffer(new DifferenceEffect(1)); ``` -これらの宣言は、`CMPWD2D` を接頭辞とする101件の診断を報告するアナライザーが検査します。シェーダーは Direct2D が受け付ける DXBC へ FXC でコンパイルします。`d3dcompiler_47.dll` は Windows に同梱されているため、このパッケージはコンパイラーを同梱しません。 +これらの宣言は、`CMPWD2D` を接頭辞とする102件の診断を報告するアナライザーが検査します。シェーダーは Direct2D が受け付ける DXBC へ FXC でコンパイルします。`d3dcompiler_47.dll` は Windows に同梱されているため、このパッケージはコンパイラーを同梱しません。 --- diff --git a/README.md b/README.md index 5678cc3bb..1b592e36e 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ The budget covers the resources the device creates itself. A device using an all ### Compile-time validation -The declarations above are checked by analyzers that report 113 diagnostics with the `CMPW` prefix, covering attribute placement, host and pipeline method shape, slot declaration, resource contracts and generated overload conflicts. Some carry a code fix. +The declarations above are checked by analyzers that report 114 diagnostics with the `CMPW` prefix, covering attribute placement, host and pipeline method shape, slot declaration, resource contracts and generated overload conflicts. Some carry a code fix. --- @@ -297,7 +297,7 @@ ReadOnlyMemory bytecode = D2D1PixelShader.LoadBytecode() ReadOnlyMemory buffer = D2D1PixelShader.GetConstantBuffer(new DifferenceEffect(1)); ``` -The declarations are checked by analyzers that report 101 diagnostics with the `CMPWD2D` prefix. Shaders are compiled to DXBC with FXC, which is what Direct2D accepts; `d3dcompiler_47.dll` ships with Windows, so the package bundles no compiler of its own. +The declarations are checked by analyzers that report 102 diagnostics with the `CMPWD2D` prefix. Shaders are compiled to DXBC with FXC, which is what Direct2D accepts; `d3dcompiler_47.dll` ships with Windows, so the package bundles no compiler of its own. --- diff --git a/src/ComputeWeave.D2D1.SourceGenerators/AnalyzerReleases.Unshipped.md b/src/ComputeWeave.D2D1.SourceGenerators/AnalyzerReleases.Unshipped.md index a3c8e253e..7c91a680f 100644 --- a/src/ComputeWeave.D2D1.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/src/ComputeWeave.D2D1.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -6,3 +6,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- CMPWD2D0101 | ComputeWeave.D2D1.Shaders | Error | [Documentation](https://github.com/routersys/ComputeWeave) +CMPWD2D0104 | ComputeWeave.D2D1.Shaders | Error | [Documentation](https://github.com/routersys/ComputeWeave) diff --git a/src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 89cecdf52..404661599 100644 --- a/src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -1557,4 +1557,24 @@ partial class DiagnosticDescriptors isEnabledByDefault: true, description: "HLSL has no recursion, so the shader compiler refuses a function that reaches itself through the functions it calls. Without this the call is written out as it stands and the shader compiler answers at the shader type, naming the function in the generated HLSL, which for a local function or a method of a custom type is a name the generator gave it. What is reported is every call on a cycle, at the call as the author wrote it, so a cycle through two declarations is reported at both of the calls closing it and removing either one resolves both. The calls followed are the ones the generated HLSL holds: a method of the shader, a method or constructor of another type the shader reaches, and a local function, which is written out whether or not it is called.", helpLinkUri: "https://github.com/routersys/ComputeWeave"); + + /// + /// Gets a for a member of the shader accessed through a qualifier where a local or a parameter of its name hides it. + /// + /// Format: "The member {0} cannot be accessed through a qualifier here in a D2D1 pixel shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be read instead)". + /// + /// + /// + /// This is the Direct2D counterpart of the compute diagnostic. The rewriting that drops the qualifier is + /// shared, so leaving one of the two out would refuse the same access on one path and not the other. + /// + public static readonly DiagnosticDescriptor ShaderMemberHiddenByLocal = new( + id: "CMPWD2D0104", + title: "Shader member hidden by a local", + messageFormat: "The member {0} cannot be accessed through a qualifier here in a D2D1 pixel shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be read instead)", + category: "ComputeWeave.D2D1.Shaders", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A member of the shader is written out under its name alone, whatever qualifier the access carried, the generated HLSL declaring it at the top level: 'this' is dropped from a field, and the type name from a static field and from a method. HLSL resolves a name by scope alone, so a local or a parameter of that name in the function the access is written in hides the member there. Without this a field read through a qualifier compiles and reads the local, so the shader computes a value C# never produces with nothing reporting it, and a method called through one fails in the shader compiler naming generated code. What is refused is an access whose qualifier would be dropped where the generated HLSL declares a local or a parameter of the same name ahead of it in the same function: a parameter, a local declared before the access, and a local declared in an argument, which the rewriting hoists ahead of the body. A local declared after the access, one of an enclosing declaration, a constant and a local function are left alone, the first two being out of scope at the access in HLSL and the last two being written out under names of their own.", + helpLinkUri: "https://github.com/routersys/ComputeWeave"); } \ No newline at end of file diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.Diagnostics.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.Diagnostics.cs index 634f4e452..b8cbe1ffd 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.Diagnostics.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.Diagnostics.cs @@ -1,3 +1,4 @@ +using System.Linq; using ComputeWeave.SourceGeneration.Extensions; using ComputeWeave.SourceGeneration.Mappings; using Microsoft.CodeAnalysis; @@ -304,6 +305,61 @@ static bool IsInnermostWidening(IOperation operation) } } + /// + /// Reports a member of the shader accessed through a qualifier where a local or a parameter of its name hides it. + /// + /// The access as the author wrote it. + /// The member of the shader the access resolves to. + /// + /// + /// A member of the shader is written out under its name alone, whatever qualifier the access carried, the + /// generated HLSL declaring it at the top level: is dropped from a field, and the type + /// name from a static field and from a method. HLSL resolves a name by scope alone, so a local or a parameter + /// of that name in the function the access is written in hides the member there, and the shader reads the + /// local where C# read the member, with nothing reporting it. + /// + /// + /// What is asked is what the generated HLSL declares ahead of the access in the same function. A parameter + /// always is. A local declared after the access is not, HLSL scoping it from its declaration, so the access + /// reads the member there. One declared in an argument is written ahead of the whole body of the function + /// holding it by the rewriting that hoists it, so it counts wherever it was written, even in a block the + /// access is outside of. A local of an enclosing declaration is left out, a local function being written + /// out as a function of its own. + /// + /// + protected void ReportShaderMemberHiddenByLocal(SyntaxNode node, ISymbol member) + { + ISymbol? function = SemanticModel.For(node).GetEnclosingSymbol(node.SpanStart, CancellationToken); + + foreach (ISymbol symbol in SemanticModel.For(node).LookupSymbols(node.SpanStart, name: member.Name)) + { + if (!SymbolEqualityComparer.Default.Equals(symbol.ContainingSymbol, function)) + { + continue; + } + + if (symbol is IParameterSymbol || + (symbol is ILocalSymbol local && local.DeclaringSyntaxReferences[0].Span.Start < node.SpanStart)) + { + Diagnostics.Add(ShaderMemberHiddenByLocal, node, member, member.Name); + + return; + } + } + + // A declaration in an argument is out of scope where C# would not see it, so it is read from the syntax + // of the function rather than from the lookup, descending into no nested function, which hoists its own + SyntaxNode? body = node.Ancestors().FirstOrDefault(static ancestor => ancestor is LocalFunctionStatementSyntax or BaseMethodDeclarationSyntax); + + if (body is not null && + body.DescendantNodes(descendant => descendant == body || descendant is not LocalFunctionStatementSyntax) + .OfType() + .Any(declaration => declaration.Designation is SingleVariableDesignationSyntax designation && designation.Identifier.ValueText == member.Name)) + { + Diagnostics.Add(ShaderMemberHiddenByLocal, node, member, member.Name); + } + } + /// public sealed override SyntaxNode? VisitAnonymousObjectCreationExpression(AnonymousObjectCreationExpressionSyntax node) { diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs index a58bb508e..bdb876c66 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs @@ -557,6 +557,9 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn // really useful anyway given that those fields are readonly, so less likely to cause observable conflicts). if (SymbolEqualityComparer.Default.Equals(fieldOperation.Field.ContainingType, ShaderType)) { + // The name alone is what is written, so a local of that name would hide the field (see HlslSourceRewriter) + ReportShaderMemberHiddenByLocal(node, fieldOperation.Field); + return updatedNode.Name; } @@ -574,6 +577,9 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn // is often the case if they're accessed from external types. So we just map the name and use that expression. if (SymbolEqualityComparer.Default.Equals(fieldOperation.Field.ContainingType, ShaderType)) { + // The same holds here, the type name being dropped (see HlslSourceRewriter) + ReportShaderMemberHiddenByLocal(node, fieldOperation.Field); + _ = HlslKnownKeywords.TryGetMappedName(fieldOperation.Field.Name, out string? mappedFieldName); return IdentifierName(mappedFieldName ?? fieldOperation.Field.Name); diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs index 084828996..7da9dac9f 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs @@ -110,6 +110,7 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn { if (SymbolEqualityComparer.Default.Equals(staticFieldOperation.Field.ContainingType, ShaderType)) { + // An initializer declares no local and has no parameter, so nothing hides the name dropped here _ = HlslKnownKeywords.TryGetMappedName(staticFieldOperation.Field.Name, out string? mappedFieldName); return IdentifierName(mappedFieldName ?? staticFieldOperation.Field.Name); diff --git a/src/ComputeWeave.SourceGenerators/AnalyzerReleases.Unshipped.md b/src/ComputeWeave.SourceGenerators/AnalyzerReleases.Unshipped.md index c78ea44d3..7bc52a71c 100644 --- a/src/ComputeWeave.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/src/ComputeWeave.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -6,3 +6,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- CMPW0129 | ComputeWeave.Shaders | Error | [Documentation](https://github.com/routersys/ComputeWeave) +CMPW0131 | ComputeWeave.Shaders | Error | [Documentation](https://github.com/routersys/ComputeWeave) diff --git a/src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 0059ef051..3f5a6f7b9 100644 --- a/src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -1719,4 +1719,20 @@ partial class DiagnosticDescriptors isEnabledByDefault: true, description: "HLSL has no recursion, so the shader compiler refuses a function that reaches itself through the functions it calls. Without this the call is written out as it stands and the shader compiler answers at the shader type, naming the function in the generated HLSL, which for a local function or a method of a custom type is a name the generator gave it. What is reported is every call on a cycle, at the call as the author wrote it, so a cycle through two declarations is reported at both of the calls closing it and removing either one resolves both. The calls followed are the ones the generated HLSL holds: a method of the shader, a method or constructor of another type the shader reaches, and a local function, which is written out whether or not it is called.", helpLinkUri: "https://github.com/routersys/ComputeWeave"); + + /// + /// Gets a for a member of the shader accessed through a qualifier where a local or a parameter of its name hides it. + /// + /// Format: "The member {0} cannot be accessed through a qualifier here in a compute shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be read instead)". + /// + /// + public static readonly DiagnosticDescriptor ShaderMemberHiddenByLocal = new( + id: "CMPW0131", + title: "Shader member hidden by a local", + messageFormat: "The member {0} cannot be accessed through a qualifier here in a compute shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be read instead)", + category: "ComputeWeave.Shaders", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A member of the shader is written out under its name alone, whatever qualifier the access carried, the generated HLSL declaring it at the top level: 'this' is dropped from a field, and the type name from a static field and from a method. HLSL resolves a name by scope alone, so a local or a parameter of that name in the function the access is written in hides the member there. Without this a field read through a qualifier compiles and reads the local, so the shader computes a value C# never produces with nothing reporting it, and a method called through one fails in the shader compiler naming generated code. What is refused is an access whose qualifier would be dropped where the generated HLSL declares a local or a parameter of the same name ahead of it in the same function: a parameter, a local declared before the access, and a local declared in an argument, which the rewriting hoists ahead of the body. A local declared after the access, one of an enclosing declaration, a constant and a local function are left alone, the first two being out of scope at the access in HLSL and the last two being written out under names of their own.", + helpLinkUri: "https://github.com/routersys/ComputeWeave"); } \ No newline at end of file diff --git a/src/ComputeWeave/README.ja.md b/src/ComputeWeave/README.ja.md index 4ad54a45b..0f4b3be21 100644 --- a/src/ComputeWeave/README.ja.md +++ b/src/ComputeWeave/README.ja.md @@ -94,7 +94,7 @@ public sealed partial class ResourceSet ## コンパイル時の検証 -以上の宣言はアナライザーが検査し、接頭辞 `CMPW` の診断113種類として報告します。対象は属性の位置、ホストとパイプラインメソッドの形、スロットの宣言、資源の契約、生成されるオーバーロードの衝突です。一部にはコード修正が付きます。実行時に識別子を持つ拒否は `IComputeDiagnostic` を実装するもの、すなわち `ComputeDiagnosticException` と `GraphicsDeviceMismatchException` だけです。その `DiagnosticId` は同じ `CMPW` 接頭辞を専用の番号帯で使います。引数の検査は識別子を持ちません。`ArgumentOutOfRangeException` をはじめとするフレームワークの例外型を送出し、代わりに引数の名前を載せます。 +以上の宣言はアナライザーが検査し、接頭辞 `CMPW` の診断114種類として報告します。対象は属性の位置、ホストとパイプラインメソッドの形、スロットの宣言、資源の契約、生成されるオーバーロードの衝突です。一部にはコード修正が付きます。実行時に識別子を持つ拒否は `IComputeDiagnostic` を実装するもの、すなわち `ComputeDiagnosticException` と `GraphicsDeviceMismatchException` だけです。その `DiagnosticId` は同じ `CMPW` 接頭辞を専用の番号帯で使います。引数の検査は識別子を持ちません。`ArgumentOutOfRangeException` をはじめとするフレームワークの例外型を送出し、代わりに引数の名前を載せます。 ## 詳細 diff --git a/src/ComputeWeave/README.md b/src/ComputeWeave/README.md index dac8b4397..93f8442af 100644 --- a/src/ComputeWeave/README.md +++ b/src/ComputeWeave/README.md @@ -94,7 +94,7 @@ Retiring a shared texture generation drains the external queue before the extern ## Compile-time validation -The declarations above are checked by analyzers that report 113 diagnostics with the `CMPW` prefix, covering attribute placement, host and pipeline method shape, slot declaration, resource contracts and generated overload conflicts. Some carry a code fix. At run time an identifier is carried only by the rejections that implement `IComputeDiagnostic`, which are `ComputeDiagnosticException` and `GraphicsDeviceMismatchException`; their `DiagnosticId` uses the same `CMPW` prefix in a number band of its own. Argument validation carries no identifier: it throws the framework exception types, `ArgumentOutOfRangeException` among them, and names the argument instead. +The declarations above are checked by analyzers that report 114 diagnostics with the `CMPW` prefix, covering attribute placement, host and pipeline method shape, slot declaration, resource contracts and generated overload conflicts. Some carry a code fix. At run time an identifier is carried only by the rejections that implement `IComputeDiagnostic`, which are `ComputeDiagnosticException` and `GraphicsDeviceMismatchException`; their `DiagnosticId` uses the same `CMPW` prefix in a number band of its own. Argument validation carries no identifier: it throws the framework exception types, `ArgumentOutOfRangeException` among them, and names the argument instead. ## More From dd0c776604a1aed23a5742fadf78b45615ec4cc6 Mon Sep 17 00:00:00 2001 From: routersys Date: Thu, 17 Sep 2026 10:17:13 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=E9=9A=A0=E3=81=95=E3=82=8C=E3=82=8B?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=90=E3=83=BC=E3=81=AE=E8=AA=AD=E3=81=BF?= =?UTF-8?q?=E3=81=AE=E6=96=AD=E3=82=8A=E3=82=92=E8=A9=A6=E9=A8=93=E3=81=A7?= =?UTF-8?q?=E5=9B=BA=E5=AE=9A=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...elShaderDescriptorGenerator_Diagnostics.cs | 67 +++ .../Shaders/ShaderMemberHiddenByLocalTests.cs | 396 ++++++++++++++++++ 2 files changed, 463 insertions(+) create mode 100644 tests/ComputeWeave.Tests.SourceGenerators/Shaders/ShaderMemberHiddenByLocalTests.cs diff --git a/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs b/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs index 794438cb2..55a7327a7 100644 --- a/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs +++ b/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs @@ -705,6 +705,73 @@ public float4 Execute() CSharpGeneratorTest.VerifyDiagnostics(source); } + /// + /// A captured field read through , with a local of its name declared ahead of it. The + /// check is shared with the compute generator, so what this pins is that the pixel shader generator answers + /// with its own identifier. + /// + [TestMethod] + public void ALocalHidingACapturedFieldIsReported() + { + const string source = """ + using ComputeWeave; + using ComputeWeave.D2D1; + using float4 = global::ComputeWeave.Float4; + + namespace MyNamespace; + + [D2DInputCount(0)] + [D2DShaderProfile(D2D1ShaderProfile.PixelShader50)] + [D2DGeneratedPixelShaderDescriptor] + internal readonly partial struct MyShader : ID2D1PixelShader + { + private readonly float time; + + public float4 Execute() + { + float time = 3.0f; + + return new float4(this.time + time, 0, 0, 1); + } + } + """; + + CSharpGeneratorTest.VerifyDiagnostics(source, "CMPWD2D0104"); + } + + /// + /// The same read with the local declared after it, which hides nothing in HLSL, so the shader compiles. + /// + [TestMethod] + public void ALocalDeclaredAfterTheAccessIsNotReported() + { + const string source = """ + using ComputeWeave; + using ComputeWeave.D2D1; + using float4 = global::ComputeWeave.Float4; + + namespace MyNamespace; + + [D2DInputCount(0)] + [D2DShaderProfile(D2D1ShaderProfile.PixelShader50)] + [D2DGeneratedPixelShaderDescriptor] + internal readonly partial struct MyShader : ID2D1PixelShader + { + private readonly float time; + + public float4 Execute() + { + float scale = this.time; + float time = 3.0f; + + return new float4(scale + time, 0, 0, 1); + } + } + """; + + CSharpGeneratorTest.VerifyDiagnostics(source); + } + /// /// An attribute of the author's own, carrying syntax the set has no verdict for, on an imported method. /// diff --git a/tests/ComputeWeave.Tests.SourceGenerators/Shaders/ShaderMemberHiddenByLocalTests.cs b/tests/ComputeWeave.Tests.SourceGenerators/Shaders/ShaderMemberHiddenByLocalTests.cs new file mode 100644 index 000000000..dfa5bc460 --- /dev/null +++ b/tests/ComputeWeave.Tests.SourceGenerators/Shaders/ShaderMemberHiddenByLocalTests.cs @@ -0,0 +1,396 @@ +using System.Collections.Immutable; +using System.Linq; +using ComputeWeave.SourceGenerators; +using ComputeWeave.Tests.SourceGenerators.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ComputeWeave.Tests.SourceGenerators.Shaders; + +/// +/// A member of the shader accessed through a qualifier where a local or a parameter of the same name is in +/// scope. The qualifier is dropped when the access is written out, so HLSL, which resolves a name by scope +/// alone, reads the local where C# read the member. +/// +/// +/// The shaders here carry a thread group size, which is what turns shader compilation on. A shape that is not +/// refused therefore has to compile as well, so a check that refused too little would arrive as a diagnostic +/// from the shader compiler rather than as a passing string match. +/// +[TestClass] +public class ShaderMemberHiddenByLocalTests +{ + /// + /// A captured field read through , with a local of its name declared ahead of it. + /// + [TestMethod] + public void ALocalHidingACapturedFieldIsReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + private readonly float amount; + + public void Execute() + { + float amount = 3.0f; + + this.buffer[0] = this.amount + amount; + } + } + """; + + AssertReportedAt(Source, "HiddenCapturedFieldTests", "this.amount"); + } + + /// + /// A static field read through the shader type, with a local of its name declared ahead of it. + /// + [TestMethod] + public void ALocalHidingAStaticFieldIsReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private static readonly float Factor = 2.0f; + + private readonly ReadWriteBuffer buffer; + + public void Execute() + { + float Factor = 3.0f; + + this.buffer[0] = Shader.Factor + Factor; + } + } + """; + + AssertReportedAt(Source, "HiddenStaticFieldTests", "Shader.Factor"); + } + + /// + /// A captured field read through in a method whose parameter has its name. + /// + [TestMethod] + public void AParameterHidingACapturedFieldIsReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + private readonly float amount; + + private float Add(float amount) => this.amount + amount; + + public void Execute() + { + this.buffer[0] = Add(1.0f); + } + } + """; + + AssertReportedAt(Source, "HiddenByParameterTests", "this.amount"); + } + + /// + /// A local initialized from the field it is named after. The local is in scope in its own initializer, + /// and the generated HLSL would read it there before anything assigned it. + /// + [TestMethod] + public void ALocalInitializedFromTheFieldItHidesIsReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + private readonly float amount; + + public void Execute() + { + float amount = this.amount; + + this.buffer[0] = amount; + } + } + """; + + AssertReportedAt(Source, "HiddenByOwnInitializerTests", "this.amount"); + } + + /// + /// A local declared in an argument after the access. The rewriting hoists that declaration ahead of the + /// body, so it hides the field wherever it was written, unlike a local declared in a statement. + /// + [TestMethod] + public void AHoistedLocalDeclaredAfterTheAccessIsReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + private readonly float amount; + + public void Execute() + { + this.buffer[0] = this.amount; + this.buffer[1] = Hlsl.Modf(2.5f, out float amount) + amount; + } + } + """; + + AssertReportedAt(Source, "HiddenByHoistedLocalTests", "this.amount"); + } + + /// + /// A local declared in an argument inside a block the access is outside of. C# does not see it at the + /// access, but the rewriting hoists it ahead of the whole body, where it hides the field for every read. + /// + [TestMethod] + public void AHoistedLocalOfAnotherBlockIsReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + private readonly float amount; + + public void Execute() + { + if (this.buffer[1] > 0.0f) + { + this.buffer[1] = Hlsl.Modf(2.5f, out float amount) + amount; + } + + this.buffer[0] = this.amount; + } + } + """; + + AssertReportedAt(Source, "HiddenByHoistedLocalOfAnotherBlockTests", "this.amount"); + } + + /// + /// A local declared after the access, in a statement. HLSL scopes it from its declaration, the way C# + /// binds a simple name, so the access ahead of it reads the field and nothing is refused. + /// + [TestMethod] + public void ALocalDeclaredAfterTheAccessIsNotReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + private readonly float amount; + + public void Execute() + { + this.buffer[0] = this.amount; + + float amount = 3.0f; + + this.buffer[1] = amount; + } + } + """; + + AssertNotReported(Source, "LaterLocalTests"); + } + + /// + /// A local of the enclosing method, with the access inside a static local function. The function is + /// written out as a function of its own, so the local is not in scope there. + /// + [TestMethod] + public void ALocalOfTheEnclosingMethodIsNotReportedInsideALocalFunction() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private static readonly float Factor = 2.0f; + + private readonly ReadWriteBuffer buffer; + + public void Execute() + { + float Factor = 3.0f; + + static float Read() => Shader.Factor; + + this.buffer[0] = Read() + Factor; + } + } + """; + + AssertNotReported(Source, "EnclosingLocalTests"); + } + + /// + /// A local declared in a block the access is outside of. It is out of scope at the access in C# and in + /// HLSL alike. + /// + [TestMethod] + public void ALocalOfAnotherBlockIsNotReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + private readonly float amount; + + public void Execute() + { + if (this.buffer[1] > 0.0f) + { + float amount = 3.0f; + + this.buffer[1] = amount; + } + + this.buffer[0] = this.amount; + } + } + """; + + AssertNotReported(Source, "SiblingBlockLocalTests"); + } + + /// + /// A constant read through the shader type beside a local of its name. A constant is written out under + /// a name of its own, so the local hides nothing and nothing is refused. + /// + [TestMethod] + public void ALocalNamedAfterAConstantIsNotReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private const float Scale = 2.0f; + + private readonly ReadWriteBuffer buffer; + + public void Execute() + { + float Scale = 3.0f; + + this.buffer[0] = Shader.Scale + Scale; + } + } + """; + + AssertNotReported(Source, "ConstantNamesakeTests"); + } + + /// + /// Asserts that one access is refused, at the access as the author wrote it and with nothing else reported. + /// + /// The shader source to generate from. + /// The assembly name to compile under. + /// The text of the access the report has to land on. + private static void AssertReportedAt(string source, string assemblyName, string access) + { + ImmutableArray diagnostics = Run(source, assemblyName).Diagnostics; + + Assert.AreEqual( + "CMPW0131", + string.Join(", ", diagnostics.Select(static diagnostic => diagnostic.Id))); + + // The report has to name the access the author wrote, which is the place to change + Assert.AreEqual(access, diagnostics[0].Location.SourceTree!.GetText().ToString(diagnostics[0].Location.SourceSpan)); + } + + /// + /// Asserts that nothing is refused and the generated HLSL compiles. + /// + /// The shader source to generate from. + /// The assembly name to compile under. + private static void AssertNotReported(string source, string assemblyName) + { + ImmutableArray diagnostics = Run(source, assemblyName).Diagnostics; + + Assert.IsTrue( + diagnostics.IsEmpty, + string.Join(", ", diagnostics.Select(static diagnostic => diagnostic.ToString()))); + } + + /// + /// Runs the generator over a source and returns the whole run result. + /// + /// The source to compile. + /// The assembly name to compile under. + /// The result of the run. + private static GeneratorRunResult Run(string source, string assemblyName) + { + CSharpCompilation compilation = CompilationHelper.CreateCompilation( + [source], + assemblyName, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); + GeneratorDriver driver = GeneratorHelper.CreateDriver(new ComputeShaderDescriptorGenerator()); + GeneratorRunResult result = driver.RunGenerators(compilation).GetRunResult().Results[0]; + + Assert.IsNull(result.Exception, result.Exception?.ToString()); + + return result; + } +} From 8d4e8fdfbff60fbab9b19c677fd0ed1ef642ed84 Mon Sep 17 00:00:00 2001 From: routersys Date: Thu, 17 Sep 2026 11:30:38 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=E4=B9=96=E9=9B=A2=E5=8F=B0=E5=B8=B3?= =?UTF-8?q?=E3=81=B8=E9=9A=A0=E3=81=95=E3=82=8C=E3=82=8B=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=90=E3=83=BC=E3=81=AE=E8=AA=AD=E3=81=BF=E3=81=AE=E8=A1=8C?= =?UTF-8?q?=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76a76c2ec..474e73c67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -760,6 +760,7 @@ The Direct2D authoring projects were removed before the first release and restor | Generated HLSL, 64 bit integer literals and constants | Writes a `long` or a `ulong` literal with the suffix the author wrote and a constant of either type as its digits, and leaves a `string` constant as an identifier. The shader compiler reads the digits as a 32 bit value, so a negative literal on the left of a comparison with an unsigned value is compared as unsigned, and a value past the 32 bit range is truncated, with no diagnostic from either compiler; the refusal of a widened operand reads the left operand alone, which a literal is not a conversion of, so a 64 bit literal on that side escapes it as well | Refuses a literal or a constant whose type the generator does not map by tracking that type, so the shader is reported with `CMPW0050`, or `CMPWD2D0041` on the Direct2D path, the way a local of that type already is. The three sites that turn a constant into an HLSL definition share one helper whose failure path is that refusal, so a string constant is refused the same way rather than left as an identifier the shader compiler cannot resolve. No shader in this repository carries such a literal or constant, so the generated files are unchanged, which was compared in full | `1c3c0ecf`, `8c5db0ab` | | Generated HLSL, native integer constants | Writes a `nint` or a `nuint` constant as its digits, the value being boxed as a 32 bit one, so the shader computes with it at 32 bits while C# computes with it at the width of the platform: a result past 32 bits wraps, and the refusal of a widened operand reads the left operand alone, so the constant on that side escapes it as a 64 bit one does | Refuses a native integer constant by tracking its type, the way a local of that type and a 64 bit constant already are, so the shader is reported with `CMPW0050`, or `CMPWD2D0041` on the Direct2D path. The one function that decides which constants have an HLSL literal takes the type of the constant as well as its value, since the value cannot tell a native integer apart. No shader in this repository carries such a constant, so the generated files are unchanged | `e33998ba` | | Generated HLSL, a call leading back to the declaration it is written in | Writes every call out as it stands and imports a declaration once, claiming its entry before the rewriting so that a method reaching itself ends the import rather than the stack, and nothing reads what that claim just found. HLSL has no recursion, so the shader compiler refuses the function, and the failure lands on the shader type as the forwarded compiler error, naming a function of the generated HLSL, which for a local function or a method of a custom type is a name the generator gave it. Six shapes were measured on the compute path and two on the Direct2D path, each accepted by the generator and refused by the shader compiler | Records every call the generated HLSL holds as it is rewritten, from the declaration being rewritten to the one the call resolves to, into one collection every rewriter for a shader shares the way the requirements are shared, and reads it once every declaration is rewritten for the calls whose target reaches back to the declaration holding them. Each is reported as `CMPW0129`, or `CMPWD2D0101` on the Direct2D path, at the call as the author wrote it, naming the callee and the declaration in C# terms. The read comes after the rewriting rather than during it because no rewriting passes through every edge of a cycle: a method of the shader is written out by the generator's own path rather than imported, a local function is lifted where it is declared, and an imported declaration is rewritten once. Every call on a cycle is reported rather than the one a traversal happens to close it on, so removing any of them resolves the report and the set does not depend on the order the declarations were reached in. A partial declaration is recorded under its defining part, which is what a call to it resolves to; a call to a declaration with no source is left to the report it already had; a static field initializer is not a function and is not a node, while the declarations it imports are recorded by the rewriter importing them | `68ebbaed` | +| Generated HLSL, a member of the shader hidden by a local | Drops the qualifier from a member of the shader when the access is written out, `this` from a field and the type name from a static field, without asking whether a local or a parameter of that name is in scope at the access. HLSL resolves a name by scope alone, so such a local hides the member there and the shader reads it in place of the member, compiling and computing a value C# never produces with nothing reporting it. A captured field read through `this` beside a local of its name, a static field read through the type beside one, and a captured field read in a method whose parameter has its name were each read back from a device as the local | Reports `CMPW0131`, or `CMPWD2D0104` on the Direct2D path, at the access as the author wrote it, from the type both rewriters derive from, so every site that drops a qualifier answers alike. What is asked is what the generated HLSL declares ahead of the access in the same function: a parameter, a local declared before the access, and a local declared in an argument, which the rewriting hoists ahead of the body. A local declared after the access is left alone, HLSL scoping it from its declaration, and so is a local of the enclosing method inside a local function, which is written out as a function of its own; a constant and a local function are written out under names of their own and hide nothing. The initializer rewriter drops the type name as well but an initializer declares no local and has no parameter, so it is left as it is. Refusing was chosen over renaming the local: the author has two names in one function that C# tells apart and HLSL cannot, and a rename would decide in silence which one the generated code reads | `2fb7f200` | The ledger is kept current by derivation, not by memory: `build/audit-upstream-divergence.ps1` lists every commit after the marker below that modifies an inherited `.cs` file under `src/` and is not cited by a row. That is the whole of what it reads. Inherited project files, documents and test sources are outside it, so a divergence in one of those reaches the ledger only because someone wrote the row by hand. Run it before a release. When the queue is not empty, either add a row or decide that the commit is not a divergence, and then move the marker. The ledger is audited through commit `c8cc970f`. From 946b6a875d403127e046b1479d1abecbc6793d3d Mon Sep 17 00:00:00 2001 From: routersys Date: Sun, 20 Sep 2026 19:02:11 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=E9=99=90=E5=AE=9A=E5=AD=90=E4=BB=98?= =?UTF-8?q?=E3=81=8D=E3=81=AE=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89=E3=81=AE?= =?UTF-8?q?=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=97=E3=82=82=E9=9A=A0=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=82=8B=E3=83=A1=E3=83=B3=E3=83=90=E3=83=BC=E3=81=A8?= =?UTF-8?q?=E3=81=97=E3=81=A6=E6=96=AD=E3=81=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Diagnostics/DiagnosticDescriptors.cs | 4 ++-- .../SyntaxRewriters/HlslSourceRewriter.Diagnostics.cs | 5 +++-- .../SyntaxRewriters/HlslSourceRewriter.cs | 11 +++++++++-- .../SyntaxRewriters/ShaderSourceRewriter.cs | 4 ++-- .../SyntaxRewriters/StaticFieldRewriter.cs | 2 +- .../Diagnostics/DiagnosticDescriptors.cs | 4 ++-- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index a5e020699..1200b22fc 100644 --- a/src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -1597,7 +1597,7 @@ partial class DiagnosticDescriptors /// /// Gets a for a member of the shader accessed through a qualifier where a local or a parameter of its name hides it. /// - /// Format: "The member {0} cannot be accessed through a qualifier here in a D2D1 pixel shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be read instead)". + /// Format: "The member {0} cannot be accessed through a qualifier here in a D2D1 pixel shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be resolved instead)". /// /// /// @@ -1607,7 +1607,7 @@ partial class DiagnosticDescriptors public static readonly DiagnosticDescriptor ShaderMemberHiddenByLocal = new( id: "CMPWD2D0104", title: "Shader member hidden by a local", - messageFormat: "The member {0} cannot be accessed through a qualifier here in a D2D1 pixel shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be read instead)", + messageFormat: "The member {0} cannot be accessed through a qualifier here in a D2D1 pixel shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be resolved instead)", category: "ComputeWeave.D2D1.Shaders", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.Diagnostics.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.Diagnostics.cs index b8cbe1ffd..891c23b6c 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.Diagnostics.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.Diagnostics.cs @@ -315,8 +315,9 @@ static bool IsInnermostWidening(IOperation operation) /// A member of the shader is written out under its name alone, whatever qualifier the access carried, the /// generated HLSL declaring it at the top level: is dropped from a field, and the type /// name from a static field and from a method. HLSL resolves a name by scope alone, so a local or a parameter - /// of that name in the function the access is written in hides the member there, and the shader reads the - /// local where C# read the member, with nothing reporting it. + /// of that name in the function the access is written in hides the member there: a field read through the + /// qualifier reads the local where C# read the field, with nothing reporting it, and a method called through + /// one fails in the shader compiler naming generated code. /// /// /// What is asked is what the generated HLSL declares ahead of the access in the same function. A parameter diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.cs index 40a383ae5..b5dadf6f2 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/HlslSourceRewriter.cs @@ -846,18 +846,25 @@ protected InvocationExpressionSyntax VisitConvertedIntrinsicArguments( /// /// Rewrites a call to a method of the shader type to name the method alone. /// + /// The original instance. /// The updated instance with tweaked syntax. + /// The method of the shader type the call resolves to. /// The invocation, with the qualifier the call was written through dropped. /// /// Both generators write the methods of the shader type out at the top level under their own names, so a /// call qualified with the shader type, with an alias of it or with names something /// the generated HLSL never declares. The name is kept as visited rather than read from the symbol, so a - /// method named after an HLSL keyword stays mapped the way its declaration is. + /// method named after an HLSL keyword stays mapped the way its declaration is. The name alone being what is + /// written, a local or a parameter of that name would hide the method, so the access is reported the way a + /// field read through a qualifier is (see the diagnostics part of this type). The report is made wherever + /// the qualifier is dropped, an initializer having no local and no parameter for it to find. /// - protected static InvocationExpressionSyntax VisitShaderMethodInvocation(InvocationExpressionSyntax updatedNode) + protected InvocationExpressionSyntax VisitShaderMethodInvocation(InvocationExpressionSyntax node, InvocationExpressionSyntax updatedNode, IMethodSymbol method) { if (updatedNode.Expression is MemberAccessExpressionSyntax qualifiedName) { + ReportShaderMemberHiddenByLocal(node.Expression, method); + return updatedNode.WithExpression(qualifiedName.Name); } diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs index 8f8fc5f67..1a8ab9fc6 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/ShaderSourceRewriter.cs @@ -829,7 +829,7 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn // A static method of the shader is written out by the generator itself, so the call is // written the way it writes the method out, under its name alone - return VisitShaderMethodInvocation(updatedNode); + return VisitShaderMethodInvocation(node, updatedNode, method); } else { @@ -884,7 +884,7 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn // through 'this' is renamed the same way if (SymbolEqualityComparer.Default.Equals(ShaderType, method.ContainingType)) { - return VisitShaderMethodInvocation(updatedNode); + return VisitShaderMethodInvocation(node, updatedNode, method); } } } diff --git a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs index fe07d9326..83cbf297a 100644 --- a/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs +++ b/src/ComputeWeave.SourceGeneration.Hlsl/SyntaxRewriters/StaticFieldRewriter.cs @@ -257,7 +257,7 @@ public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyn return VisitImportedStaticMethodInvocation(node, updatedNode, method); } - return VisitShaderMethodInvocation(updatedNode); + return VisitShaderMethodInvocation(node, updatedNode, method); } } diff --git a/src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 992b13003..6a6c22d8f 100644 --- a/src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -1739,13 +1739,13 @@ partial class DiagnosticDescriptors /// /// Gets a for a member of the shader accessed through a qualifier where a local or a parameter of its name hides it. /// - /// Format: "The member {0} cannot be accessed through a qualifier here in a compute shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be read instead)". + /// Format: "The member {0} cannot be accessed through a qualifier here in a compute shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be resolved instead)". /// /// public static readonly DiagnosticDescriptor ShaderMemberHiddenByLocal = new( id: "CMPW0131", title: "Shader member hidden by a local", - messageFormat: "The member {0} cannot be accessed through a qualifier here in a compute shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be read instead)", + messageFormat: "The member {0} cannot be accessed through a qualifier here in a compute shader (the generated HLSL drops the qualifier, and a local variable or a parameter named '{1}' declared in the same function would be resolved instead)", category: "ComputeWeave.Shaders", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, From d1189b48666608e613f9cae014dd821d95a4bd97 Mon Sep 17 00:00:00 2001 From: routersys Date: Sun, 20 Sep 2026 19:07:05 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E9=9A=A0=E3=81=95=E3=82=8C=E3=82=8B?= =?UTF-8?q?=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89=E3=81=AE=E5=91=BC=E3=81=B3?= =?UTF-8?q?=E5=87=BA=E3=81=97=E3=81=AE=E6=96=AD=E3=82=8A=E3=82=92=E8=A9=A6?= =?UTF-8?q?=E9=A8=93=E3=81=A7=E5=9B=BA=E5=AE=9A=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...elShaderDescriptorGenerator_Diagnostics.cs | 33 +++++ .../Shaders/ShaderMemberHiddenByLocalTests.cs | 132 +++++++++++++++++- 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs b/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs index 6f6a950f4..7b170da8d 100644 --- a/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs +++ b/tests/ComputeWeave.D2D1.Tests.SourceGenerators/Test_D2DPixelShaderDescriptorGenerator_Diagnostics.cs @@ -866,6 +866,39 @@ public float4 Execute() CSharpGeneratorTest.VerifyDiagnostics(source); } + /// + /// A static method called through the shader type, with a local of its name declared ahead of the call. + /// The call is renamed by the same shared rewriting as the read above, so this pins the answer for a method. + /// + [TestMethod] + public void ALocalHidingAStaticMethodIsReported() + { + const string source = """ + using ComputeWeave; + using ComputeWeave.D2D1; + using float4 = global::ComputeWeave.Float4; + + namespace MyNamespace; + + [D2DInputCount(0)] + [D2DShaderProfile(D2D1ShaderProfile.PixelShader50)] + [D2DGeneratedPixelShaderDescriptor] + internal readonly partial struct MyShader : ID2D1PixelShader + { + private static float Twice(float value) => value * 2.0f; + + public float4 Execute() + { + float Twice = 3.0f; + + return new float4(MyShader.Twice(1.0f) + Twice, 0, 0, 1); + } + } + """; + + CSharpGeneratorTest.VerifyDiagnostics(source, "CMPWD2D0104"); + } + /// /// An attribute of the author's own, carrying syntax the set has no verdict for, on an imported method. /// diff --git a/tests/ComputeWeave.Tests.SourceGenerators/Shaders/ShaderMemberHiddenByLocalTests.cs b/tests/ComputeWeave.Tests.SourceGenerators/Shaders/ShaderMemberHiddenByLocalTests.cs index dfa5bc460..85d89200a 100644 --- a/tests/ComputeWeave.Tests.SourceGenerators/Shaders/ShaderMemberHiddenByLocalTests.cs +++ b/tests/ComputeWeave.Tests.SourceGenerators/Shaders/ShaderMemberHiddenByLocalTests.cs @@ -11,7 +11,7 @@ namespace ComputeWeave.Tests.SourceGenerators.Shaders; /// /// A member of the shader accessed through a qualifier where a local or a parameter of the same name is in /// scope. The qualifier is dropped when the access is written out, so HLSL, which resolves a name by scope -/// alone, reads the local where C# read the member. +/// alone, reads the local where C# read a field and names it where C# called a method. /// /// /// The shaders here carry a thread group size, which is what turns shader compilation on. A shape that is not @@ -207,6 +207,70 @@ public void Execute() AssertReportedAt(Source, "HiddenByHoistedLocalOfAnotherBlockTests", "this.amount"); } + /// + /// A static method called through the shader type, with a local of its name declared ahead of the call. + /// The generated HLSL names the local there, which is no function to call. + /// + [TestMethod] + public void ALocalHidingAStaticMethodIsReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + + private static float Twice(float value) => value * 2.0f; + + public void Execute() + { + float Twice = 3.0f; + + this.buffer[0] = Shader.Twice(1.0f) + Twice; + } + } + """; + + AssertReportedAt(Source, "HiddenStaticMethodTests", "Shader.Twice"); + } + + /// + /// An instance method called through , with a local of its name declared ahead of + /// the call. The method is written out the way a static one is, so the local hides it the same way. + /// + [TestMethod] + public void ALocalHidingAnInstanceMethodIsReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + + private float Twice(float value) => value * 2.0f; + + public void Execute() + { + float Twice = 3.0f; + + this.buffer[0] = this.Twice(1.0f) + Twice; + } + } + """; + + AssertReportedAt(Source, "HiddenInstanceMethodTests", "this.Twice"); + } + /// /// A local declared after the access, in a statement. HLSL scopes it from its declaration, the way C# /// binds a simple name, so the access ahead of it reads the field and nothing is refused. @@ -342,6 +406,72 @@ public void Execute() AssertNotReported(Source, "ConstantNamesakeTests"); } + /// + /// A local declared after a call through the shader type. The function is what the name resolves to + /// until the declaration, in HLSL as in C#, so the call ahead of it is left alone. + /// + [TestMethod] + public void ALocalDeclaredAfterTheCallIsNotReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + + private static float Twice(float value) => value * 2.0f; + + public void Execute() + { + this.buffer[0] = Shader.Twice(1.0f); + + float Twice = 3.0f; + + this.buffer[1] = Twice; + } + } + """; + + AssertNotReported(Source, "LaterLocalAfterCallTests"); + } + + /// + /// A local function named after a method called through the shader type. The local function is written + /// out under a name of its own, so it hides nothing and nothing is refused. + /// + [TestMethod] + public void ALocalFunctionNamedAfterAMethodIsNotReported() + { + const string Source = """ + using ComputeWeave; + + namespace Shaders; + + [ThreadGroupSize(DefaultThreadGroupSizes.X)] + [GeneratedComputeShaderDescriptor] + internal readonly partial struct Shader : IComputeShader + { + private readonly ReadWriteBuffer buffer; + + private static float Twice(float value) => value * 2.0f; + + public void Execute() + { + static float Twice(float value) => value + value; + + this.buffer[0] = Shader.Twice(1.0f) + Twice(2.0f); + } + } + """; + + AssertNotReported(Source, "LocalFunctionNamesakeTests"); + } + /// /// Asserts that one access is refused, at the access as the author wrote it and with nothing else reported. /// From 38cc6ef86d5db7a5eb355551e1d8604815c801ff Mon Sep 17 00:00:00 2001 From: routersys Date: Sun, 20 Sep 2026 19:30:25 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=E4=B9=96=E9=9B=A2=E5=8F=B0=E5=B8=B3?= =?UTF-8?q?=E3=81=AE=E9=9A=A0=E3=81=95=E3=82=8C=E3=82=8B=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=90=E3=83=BC=E3=81=AE=E8=A1=8C=E3=81=B8=E3=83=A1=E3=82=BD?= =?UTF-8?q?=E3=83=83=E3=83=89=E3=81=AE=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=97?= =?UTF-8?q?=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 192310560..7833104d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -763,7 +763,7 @@ The Direct2D authoring projects were removed before the first release and restor | Generated HLSL, a call leading back to the declaration it is written in | Writes every call out as it stands and imports a declaration once, claiming its entry before the rewriting so that a method reaching itself ends the import rather than the stack, and nothing reads what that claim just found. HLSL has no recursion, so the shader compiler refuses the function, and the failure lands on the shader type as the forwarded compiler error, naming a function of the generated HLSL, which for a local function or a method of a custom type is a name the generator gave it. Six shapes were measured on the compute path and two on the Direct2D path, each accepted by the generator and refused by the shader compiler | Records every call the generated HLSL holds as it is rewritten, from the declaration being rewritten to the one the call resolves to, into one collection every rewriter for a shader shares the way the requirements are shared, and reads it once every declaration is rewritten for the calls whose target reaches back to the declaration holding them. Each is reported as `CMPW0129`, or `CMPWD2D0101` on the Direct2D path, at the call as the author wrote it, naming the callee and the declaration in C# terms. The read comes after the rewriting rather than during it because no rewriting passes through every edge of a cycle: a method of the shader is written out by the generator's own path rather than imported, a local function is lifted where it is declared, and an imported declaration is rewritten once. Every call on a cycle is reported rather than the one a traversal happens to close it on, so removing any of them resolves the report and the set does not depend on the order the declarations were reached in. A partial declaration is recorded under its defining part, which is what a call to it resolves to; a call to a declaration with no source is left to the report it already had; a static field initializer is not a function and is not a node, while the declarations it imports are recorded by the rewriter importing them | `68ebbaed` | | Generated HLSL, a thread synchronization in a pixel shader | Maps the six memory barriers the shared `Hlsl` type declares for the compute shaders onto their HLSL names on the Direct2D path the way it maps every intrinsic, and writes the call out. A pixel shader runs with no thread group to synchronize, so FXC refuses five of the six under every pixel shader profile with `X3664`, and the failure lands on the shader type as the forwarded compiler error, naming the intrinsic but not the call. The compute path refuses the intrinsics of the pixel stage through an analyzer, so the two products did not mirror each other | Reports `CMPWD2D0102` at the call, from the shared rewriter under the Direct2D compilation symbol, the way the compute path refuses a matrix given to an intrinsic with an out parameter under its own symbol. The names are held by the Direct2D partial of the known methods, with `DeviceMemoryBarrier` left out because FXC accepts it, which was measured one name at a time. The report is on the shader rewriter alone: a barrier returns nothing, so no static field initializer can hold one, and a method an initializer imports is rewritten by the shader rewriter like any other. An analyzer was not chosen, because one would flag a compute shader in a project referencing both products, whereas the rewriter refuses only what the Direct2D generator writes out | `c45fe471`, `e982d57d` | | Generated HLSL, a call to a method of the shader through a qualifier | Leaves a static method of the shader for the generator to write out and writes the call to it as it stands, so a call qualified with the shader type, with an alias of it, with its namespace or with `global::` keeps the qualifier in the generated HLSL, where nothing declares it. An instance method called through `this` answers the same way, `this` being dropped from a field access and from nothing else. The same call written by name alone compiles, so how the author spells it decides whether the shader does, and what it fails with is the shader compiler naming a line of generated HLSL rather than the call the author wrote. A method declared outside the shader has no other way to call one of the shader's static methods | Writes the call under the name alone, whatever the qualifier, through one helper on the type both rewriters derive from, so the shader body, a static field initializer and every declaration either of them imports answer alike, and the static and the instance paths of the body rewriter reach the same rename. The name is kept as visited rather than read from the symbol: an identifier that collides with an HLSL keyword is mapped as it is visited and the declaration is written under the mapped name, so reading the symbol would leave that one spelling uncompilable, which a test pins and a mutation writing the symbol's name was measured to fail. Measured over fifteen shapes on the compute path and ten on the Direct2D one, every qualified call failing in the shader compiler before the change and compiling after it, and every unqualified one writing the same HLSL either way. The generated sources of the whole solution are unchanged across the change, no shader in the tree writing such a call, which before the change none could | `3210e52f` | -| Generated HLSL, a member of the shader hidden by a local | Drops the qualifier from a member of the shader when the access is written out, `this` from a field and the type name from a static field, without asking whether a local or a parameter of that name is in scope at the access. HLSL resolves a name by scope alone, so such a local hides the member there and the shader reads it in place of the member, compiling and computing a value C# never produces with nothing reporting it. A captured field read through `this` beside a local of its name, a static field read through the type beside one, and a captured field read in a method whose parameter has its name were each read back from a device as the local | Reports `CMPW0131`, or `CMPWD2D0104` on the Direct2D path, at the access as the author wrote it, from the type both rewriters derive from, so every site that drops a qualifier answers alike. What is asked is what the generated HLSL declares ahead of the access in the same function: a parameter, a local declared before the access, and a local declared in an argument, which the rewriting hoists ahead of the body. A local declared after the access is left alone, HLSL scoping it from its declaration, and so is a local of the enclosing method inside a local function, which is written out as a function of its own; a constant and a local function are written out under names of their own and hide nothing. The initializer rewriter drops the type name as well but an initializer declares no local and has no parameter, so it is left as it is. Refusing was chosen over renaming the local: the author has two names in one function that C# tells apart and HLSL cannot, and a rename would decide in silence which one the generated code reads | `2fb7f200` | +| Generated HLSL, a member of the shader hidden by a local | Drops the qualifier from a member of the shader when the access is written out, `this` from a field and the type name from a static field, without asking whether a local or a parameter of that name is in scope at the access. HLSL resolves a name by scope alone, so such a local hides the member there and the shader reads it in place of the member, compiling and computing a value C# never produces with nothing reporting it. A captured field read through `this` beside a local of its name, a static field read through the type beside one, and a captured field read in a method whose parameter has its name were each read back from a device as the local. A call to a method of the shader written through a qualifier, which the fork drops as the row for a call to a method of the shader through a qualifier records, is hidden the same way and fails in the shader compiler with a message naming generated code | Reports `CMPW0131`, or `CMPWD2D0104` on the Direct2D path, at the access as the author wrote it, from the type both rewriters derive from, so every site that drops a qualifier answers alike: the two field sites of the body rewriter, and the shared helper that drops the qualifier from a call to a method of the shader, which the body rewriter and the initializer rewriter both go through. What is asked is what the generated HLSL declares ahead of the access in the same function: a parameter, a local declared before the access, and a local declared in an argument, which the rewriting hoists ahead of the body. A local declared after the access is left alone, HLSL scoping it from its declaration, and so is a local of the enclosing method inside a local function, which is written out as a function of its own; a constant and a local function are written out under names of their own and hide nothing. The initializer rewriter drops the type name of a static field as well but an initializer declares no local and has no parameter, so that site is left as it is, and the helper finds nothing there for the same reason. Refusing was chosen over renaming the local: the author has two names in one function that C# tells apart and HLSL cannot, and a rename would decide in silence which one the generated code reads | `2fb7f200`, `946b6a87` | The ledger is kept current by derivation, not by memory: `build/audit-upstream-divergence.ps1` lists every commit after the marker below that modifies an inherited `.cs` file under `src/` and is not cited by a row. That is the whole of what it reads. Inherited project files, documents and test sources are outside it, so a divergence in one of those reaches the ledger only because someone wrote the row by hand. Run it before a release. When the queue is not empty, either add a row or decide that the commit is not a divergence, and then move the marker. The ledger is audited through commit `c8cc970f`.