diff --git a/benchmarks/IspcSharp.Benchmarks/IspcSharp.Benchmarks.csproj b/benchmarks/IspcSharp.Benchmarks/IspcSharp.Benchmarks.csproj index 28a66d8..d2795a6 100644 --- a/benchmarks/IspcSharp.Benchmarks/IspcSharp.Benchmarks.csproj +++ b/benchmarks/IspcSharp.Benchmarks/IspcSharp.Benchmarks.csproj @@ -1,4 +1,4 @@ - + Exe @@ -10,7 +10,7 @@ - + diff --git a/src/IspcSharp.Generators/AosAccessAnalyzer.cs b/src/IspcSharp.Generators/Analyzers/AosAccessAnalyzer.cs similarity index 82% rename from src/IspcSharp.Generators/AosAccessAnalyzer.cs rename to src/IspcSharp.Generators/Analyzers/AosAccessAnalyzer.cs index ebd2d22..d88a514 100644 --- a/src/IspcSharp.Generators/AosAccessAnalyzer.cs +++ b/src/IspcSharp.Generators/Analyzers/AosAccessAnalyzer.cs @@ -5,7 +5,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace IspcSharp.Generators; +namespace IspcSharp.Generators.Analyzers; /// /// Warns about Array-of-Structs access patterns inside SPMD kernels, the single most @@ -19,16 +19,8 @@ namespace IspcSharp.Generators; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class AosAccessAnalyzer : DiagnosticAnalyzer { - public static readonly DiagnosticDescriptor AosAccess = new( - "ISPC100", - "Array-of-Structs access in SPMD kernel", - "'{0}' accesses a field through an indexed element, an AoS pattern that becomes a per-lane gather. Restructure to Structure-of-Arrays (separate float[] per field, or SoaFloat2/SoaFloat3) for contiguous vector loads.", - "IspcSharp.Performance", - DiagnosticSeverity.Warning, - isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics - => ImmutableArray.Create(AosAccess); + => [Descriptors.AosAccess]; public override void Initialize(AnalysisContext context) { @@ -52,7 +44,7 @@ private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext ctx) if (!IsInsideSpmdKernel(ma)) return; - ctx.ReportDiagnostic(Diagnostic.Create(AosAccess, ma.GetLocation(), ma.ToString())); + ctx.ReportDiagnostic(Diagnostic.Create(Descriptors.AosAccess, ma.GetLocation(), ma.ToString())); } private static bool IsInsideSpmdKernel(SyntaxNode node) diff --git a/src/IspcSharp.Generators/Analyzers/SpmdDiagnosticsAnalyzer.cs b/src/IspcSharp.Generators/Analyzers/SpmdDiagnosticsAnalyzer.cs new file mode 100644 index 0000000..769f91b --- /dev/null +++ b/src/IspcSharp.Generators/Analyzers/SpmdDiagnosticsAnalyzer.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using IspcSharp.Generators.Exceptions; +using IspcSharp.Generators.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace IspcSharp.Generators.Analyzers; + +/// +/// Reports every [Spmd]/[SpmdFunction] diagnostic (ISPC001–ISPC005 shape/support errors and +/// the ISPC101–ISPC104 performance warnings) +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class SpmdDiagnosticsAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => [ + Descriptors.NotPartial, + Descriptors.BadShape, + Descriptors.Unsupported, + Descriptors.BadParam, + Descriptors.NoParallel, + Descriptors.GatherPerf, + Descriptors.ScatterPerf, + Descriptors.IntDividePerf, + Descriptors.DoubleConvertPerf + ]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(static compilationContext => + { + var tables = new Lazy<(Dictionary Structs, Dictionary Functions)>( + () => BuildTables(compilationContext.Compilation)); + + compilationContext.RegisterSyntaxNodeAction( + ctx => AnalyzeMethod(ctx, tables), + SyntaxKind.MethodDeclaration); + }); + } + + private static void AnalyzeMethod( + SyntaxNodeAnalysisContext ctx, + Lazy<(Dictionary Structs, Dictionary Functions)> tables) + { + var method = (MethodDeclarationSyntax)ctx.Node; + bool isKernel = SpmdGenerator.HasAttribute(method.AttributeLists, "Spmd"); + bool isFunction = SpmdGenerator.HasAttribute(method.AttributeLists, "SpmdFunction"); + if (!isKernel && !isFunction) + return; + + var (structMap, fnMap) = tables.Value; + try + { + _ = isKernel + ? SpmdGenerator.GenerateKernelSource( + KernelInfo.From(method), method, structMap, fnMap, ctx.ReportDiagnostic) + : SpmdGenerator.GenerateFunctionSource( + FunctionInfo.From(method), method, structMap, fnMap, ctx.ReportDiagnostic); + } + catch (UnsupportedConstructException ex) + { + ctx.ReportDiagnostic(Diagnostic.Create( + Descriptors.Unsupported, ex.Location ?? method.GetLocation(), + method.Identifier.Text, ex.What)); + } + } + + /// + /// Syntax-only scan for [SpmdStruct]/[SpmdFunction] declarations across the compilation + /// (the same syntactic parse the generator's transforms use). + /// + private static (Dictionary Structs, Dictionary Functions) BuildTables( + Compilation compilation) + { + var structs = new List(); + var functions = new List(); + foreach (var tree in compilation.SyntaxTrees) + { + foreach (var node in tree.GetRoot().DescendantNodes()) + { + switch (node) + { + case StructDeclarationSyntax s when SpmdGenerator.HasAttribute(s.AttributeLists, "SpmdStruct"): + if (StructInfo.From(s) is { } si) + structs.Add(si); + break; + case MethodDeclarationSyntax m when SpmdGenerator.HasAttribute(m.AttributeLists, "SpmdFunction"): + functions.Add(FunctionInfo.From(m)); + break; + default: + break; + } + } + } + + return (SpmdGenerator.BuildStructMap(structs), SpmdGenerator.BuildFunctionMap(functions)); + } +} diff --git a/src/IspcSharp.Generators/Contexts/LoopContext.cs b/src/IspcSharp.Generators/Contexts/LoopContext.cs new file mode 100644 index 0000000..940f975 --- /dev/null +++ b/src/IspcSharp.Generators/Contexts/LoopContext.cs @@ -0,0 +1,32 @@ +namespace IspcSharp.Generators.Contexts; + +/// +/// Tracks break/continue state for the innermost loop being emitted. +/// For uniform loops (plain C# for), break/continue map to C# keywords. +/// For varying loops (mask iteration), break/continue manipulate masks. +/// +internal readonly struct LoopContext +{ + public readonly string BreakMask; + public readonly string ContinueMask; + public readonly string LoopMask; + public readonly bool IsUniform; + + public LoopContext(string breakMask, string continueMask, string loopMask) + { + BreakMask = breakMask; + ContinueMask = continueMask; + LoopMask = loopMask; + IsUniform = false; + } + + private LoopContext(bool isUniform) + { + BreakMask = ""; + ContinueMask = ""; + LoopMask = ""; + IsUniform = isUniform; + } + + public static LoopContext Uniform() => new(true); +} \ No newline at end of file diff --git a/src/IspcSharp.Generators/Contexts/ScaffoldContext.cs b/src/IspcSharp.Generators/Contexts/ScaffoldContext.cs new file mode 100644 index 0000000..b6df17f --- /dev/null +++ b/src/IspcSharp.Generators/Contexts/ScaffoldContext.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using IspcSharp.Generators.Models; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace IspcSharp.Generators.Contexts; + +internal sealed class ScaffoldContext( + CommonForEachStatementSyntax fe, + string loopVar, + string startExpr, + string endExpr, + string vectorBody, + string scalarBody, + List reductions, + string laneCountExpr, + bool doubleMode, + bool longMode, + int unroll, + bool hasLaneReturns) +{ + public readonly CommonForEachStatementSyntax Fe = fe; + public readonly string LoopVar = loopVar; + public readonly string StartExpr = startExpr; + public readonly string EndExpr = endExpr; + public readonly string VectorBody = vectorBody; + public readonly string ScalarBody = scalarBody; + public readonly string LaneCountExpr = laneCountExpr; + public readonly List Reductions = reductions; + public readonly bool DoubleMode = doubleMode; + public readonly bool LongMode = longMode; + public readonly bool HasLaneReturns = hasLaneReturns; + public readonly int Unroll = unroll; +} diff --git a/src/IspcSharp.Generators/Descriptors.cs b/src/IspcSharp.Generators/Descriptors.cs new file mode 100644 index 0000000..dbf4b77 --- /dev/null +++ b/src/IspcSharp.Generators/Descriptors.cs @@ -0,0 +1,59 @@ +using Microsoft.CodeAnalysis; + +namespace IspcSharp.Generators; + +internal static class Descriptors +{ + internal static readonly DiagnosticDescriptor NotPartial = new( + "ISPC001", "Containing type must be partial", + "[Spmd] method '{0}': the containing type must be declared 'partial' so generated code can be added", + "IspcSharp", DiagnosticSeverity.Error, true); + + internal static readonly DiagnosticDescriptor BadShape = new( + "ISPC002", "Unsupported [Spmd] method shape", + "[Spmd] method '{0}': body must be [optional float/int/double locals], one 'foreach (var i in Spmd.Range(...))' or 'foreach (var (x, y) in Spmd.Range2D(...))' loop, then [optional trailing statements]", + "IspcSharp", DiagnosticSeverity.Error, true); + + internal static readonly DiagnosticDescriptor Unsupported = new( + "ISPC003", "Construct not vectorizable", + "[Spmd] method '{0}': {1} is not supported by the SPMD vectorizer (v0.2 subset). Rewrite it, or use the IspcSharp runtime API directly for full control.", + "IspcSharp", DiagnosticSeverity.Error, true); + + internal static readonly DiagnosticDescriptor BadParam = new( + "ISPC004", "Unsupported parameter type", + "[Spmd] method '{0}': parameter '{1}' has unsupported type '{2}'. Supported: float[]/int[]/double[], Span/ReadOnlySpan of float/int/double, and uniform float/int/double.", + "IspcSharp", DiagnosticSeverity.Error, true); + + internal static readonly DiagnosticDescriptor NoParallel = new( + "ISPC005", "Parallel variant skipped", + "[Spmd] method '{0}': {0}_ParallelSimd was not generated: {1}", + "IspcSharp", DiagnosticSeverity.Info, true); + + internal static readonly DiagnosticDescriptor GatherPerf = new( + "ISPC101", "Gather (non-contiguous load) in SPMD kernel", + "'{0}' uses a lane-varying index, lowered to a per-lane gather (Memory.Gather), not a contiguous vector load. Make it contiguous (loop-variable/affine index, a transposed or SoA layout, or presorted indices) to avoid the gather.", + "IspcSharp.Performance", DiagnosticSeverity.Warning, true); + + internal static readonly DiagnosticDescriptor ScatterPerf = new( + "ISPC102", "Scatter (non-contiguous store) in SPMD kernel", + "'{0}' is a lane-varying indexed store, lowered to a per-active-lane scatter (Memory.Scatter); .NET has no hardware scatter instruction. Restructure to a contiguous write where possible.", + "IspcSharp.Performance", DiagnosticSeverity.Warning, true); + + internal static readonly DiagnosticDescriptor IntDividePerf = new( + "ISPC103", "Per-lane integer divide in SPMD kernel", + "integer '{0}' has no SIMD instruction and runs as a per-lane scalar loop. Expect scalar-ish throughput here; if the divisor is a constant power of two use a shift/mask instead.", + "IspcSharp.Performance", DiagnosticSeverity.Warning, true); + + internal static readonly DiagnosticDescriptor DoubleConvertPerf = new( + "ISPC104", "Double↔integer conversion in SPMD kernel", + "'{0}' converts between double and 64-bit integer lanes ((int)/(long) cast), which has no encoding before AVX-512DQ and runs per-lane on AVX2 (Zen 1–3, Haswell–Comet Lake). Keep the value in double, or move the conversion out of the hot loop.", + "IspcSharp.Performance", DiagnosticSeverity.Warning, true); + + internal static readonly DiagnosticDescriptor AosAccess = new( + "ISPC100", + "Array-of-Structs access in SPMD kernel", + "'{0}' accesses a field through an indexed element, an AoS pattern that becomes a per-lane gather. Restructure to Structure-of-Arrays (separate float[] per field, or SoaFloat2/SoaFloat3) for contiguous vector loads.", + "IspcSharp.Performance", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); +} diff --git a/src/IspcSharp.Generators/EquatableReadOnlyList.cs b/src/IspcSharp.Generators/EquatableReadOnlyList.cs new file mode 100644 index 0000000..34aaaf6 --- /dev/null +++ b/src/IspcSharp.Generators/EquatableReadOnlyList.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace IspcSharp.Generators; + +/// +/// A read-only list with structural (sequence) equality. Incremental-pipeline models must be +/// equatable for the driver to cache them; a plain or +/// compares by reference and +/// defeats caching, so models hold their collections through this wrapper instead. +/// +internal readonly struct EquatableReadOnlyList(IReadOnlyList? collection) + : IEquatable>, IReadOnlyList +{ + private IReadOnlyList Collection => collection ?? []; + + public T this[int index] => Collection[index]; + + public int Count => Collection.Count; + + public bool Equals(EquatableReadOnlyList other) + => this.SequenceEqual(other); + + public override bool Equals(object? obj) + => obj is EquatableReadOnlyList other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + foreach (var item in Collection) + hash = (hash * 31) + (item?.GetHashCode() ?? 0); + return hash; + } + } + + /// + /// Index of the first item matching , or -1. + /// + public int FindIndex(Func predicate) + { + for (int i = 0; i < Collection.Count; i++) + { + if (predicate(Collection[i])) + return i; + } + + return -1; + } + + public IEnumerator GetEnumerator() => Collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public static bool operator ==(EquatableReadOnlyList left, EquatableReadOnlyList right) + => left.Equals(right); + + public static bool operator !=(EquatableReadOnlyList left, EquatableReadOnlyList right) + => !left.Equals(right); +} diff --git a/src/IspcSharp.Generators/Exceptions/UnsupportedConstructException.cs b/src/IspcSharp.Generators/Exceptions/UnsupportedConstructException.cs new file mode 100644 index 0000000..ff5bf2a --- /dev/null +++ b/src/IspcSharp.Generators/Exceptions/UnsupportedConstructException.cs @@ -0,0 +1,10 @@ +using System; +using Microsoft.CodeAnalysis; + +namespace IspcSharp.Generators.Exceptions; + +internal sealed class UnsupportedConstructException(string what, Location? loc = null) : Exception +{ + public string What { get; } = what; + public Location? Location { get; } = loc; +} \ No newline at end of file diff --git a/src/IspcSharp.Generators/IspcSharp.Generators.csproj b/src/IspcSharp.Generators/IspcSharp.Generators.csproj index 70e7d82..d7a507f 100644 --- a/src/IspcSharp.Generators/IspcSharp.Generators.csproj +++ b/src/IspcSharp.Generators/IspcSharp.Generators.csproj @@ -34,8 +34,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -43,7 +43,7 @@ - + diff --git a/src/IspcSharp.Generators/Models/DeclarationModel.cs b/src/IspcSharp.Generators/Models/DeclarationModel.cs new file mode 100644 index 0000000..23b60a1 --- /dev/null +++ b/src/IspcSharp.Generators/Models/DeclarationModel.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace IspcSharp.Generators.Models; + +/// +/// Extracts the container facts a companion needs (type header line, partial-ness, namespace) +/// from a declaration's parents, so the models above never have to hold the syntax tree. +/// +internal static class DeclarationModel +{ + public static (string TypeHeader, bool TypeIsPartial, string Namespace) ContainerOf(MethodDeclarationSyntax m) + { + if (m.Parent is not TypeDeclarationSyntax type) + return ("", false, ""); + + bool isPartial = type.Modifiers.Any(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PartialKeyword); + string header = $"{type.Modifiers} {type.Keyword.Text} {type.Identifier.Text}"; + + var parts = new List(); + for (var n = type.Parent; n != null; n = n.Parent) + { + if (n is BaseNamespaceDeclarationSyntax nds) + parts.Insert(0, nds.Name.ToString()); + } + + return (header, isPartial, string.Join(".", parts)); + } +} diff --git a/src/IspcSharp.Generators/Models/FunctionInfo.cs b/src/IspcSharp.Generators/Models/FunctionInfo.cs new file mode 100644 index 0000000..9274bb6 --- /dev/null +++ b/src/IspcSharp.Generators/Models/FunctionInfo.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace IspcSharp.Generators.Models; + +/// +/// A [SpmdFunction] helper: its declaration syntax and parsed signature. +/// +internal sealed record FunctionInfo( + string Name, + string ReturnType, + EquatableReadOnlyList<(string Name, string Type)> Parameters, + string DeclarationText, + string TypeHeader, + bool TypeIsPartial, + string Namespace) +{ + public static FunctionInfo From(MethodDeclarationSyntax m) + { + var ps = m.ParameterList.Parameters + .Select(p => (p.Identifier.Text, p.Type?.ToString().Trim() ?? "")) + .ToList(); + var (header, isPartial, ns) = DeclarationModel.ContainerOf(m); + return new FunctionInfo( + m.Identifier.Text, + m.ReturnType.ToString().Trim(), + new EquatableReadOnlyList<(string Name, string Type)>(ps), + m.ToFullString(), + header, + isPartial, + ns); + } +} diff --git a/src/IspcSharp.Generators/Models/KernelInfo.cs b/src/IspcSharp.Generators/Models/KernelInfo.cs new file mode 100644 index 0000000..9d5eae9 --- /dev/null +++ b/src/IspcSharp.Generators/Models/KernelInfo.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace IspcSharp.Generators.Models; + +/// +/// A [Spmd] kernel: the method's source text plus its container info. +/// Same text-not-syntax rule as . +/// +internal sealed record KernelInfo( + string Name, + string MethodText, + string TypeHeader, + bool TypeIsPartial, + string Namespace) +{ + public static KernelInfo From(MethodDeclarationSyntax m) + { + var (header, isPartial, ns) = DeclarationModel.ContainerOf(m); + return new KernelInfo(m.Identifier.Text, m.ToFullString(), header, isPartial, ns); + } +} diff --git a/src/IspcSharp.Generators/Models/Kind.cs b/src/IspcSharp.Generators/Models/Kind.cs new file mode 100644 index 0000000..e122843 --- /dev/null +++ b/src/IspcSharp.Generators/Models/Kind.cs @@ -0,0 +1,3 @@ +namespace IspcSharp.Generators.Models; + +internal enum Kind { F, I, D, L } \ No newline at end of file diff --git a/src/IspcSharp.Generators/Models/ParamInfo.cs b/src/IspcSharp.Generators/Models/ParamInfo.cs new file mode 100644 index 0000000..0b3e830 --- /dev/null +++ b/src/IspcSharp.Generators/Models/ParamInfo.cs @@ -0,0 +1,48 @@ +namespace IspcSharp.Generators.Models; + +internal readonly struct ParamInfo(string name, ParamKind kind, string typeText, string? structType = null) +{ + public readonly string Name = name; + public readonly ParamKind PKind = kind; + public readonly string TypeText = typeText; + + /// + /// Element struct type name for a parameter. + /// + public readonly string? StructType = structType; + + public bool IsStructBuffer => PKind == ParamKind.StructArray; + + public bool Is2D => PKind is ParamKind.FloatArray2D or ParamKind.IntArray2D or ParamKind.DoubleArray2D or ParamKind.LongArray2D; + + public bool IsBuffer => PKind is ParamKind.FloatArray or ParamKind.FloatSpan or ParamKind.FloatReadOnlySpan + or ParamKind.IntArray or ParamKind.IntSpan or ParamKind.IntReadOnlySpan + or ParamKind.DoubleArray or ParamKind.DoubleSpan or ParamKind.DoubleReadOnlySpan + or ParamKind.LongArray or ParamKind.LongSpan or ParamKind.LongReadOnlySpan + or ParamKind.FloatArray2D or ParamKind.IntArray2D or ParamKind.DoubleArray2D or ParamKind.LongArray2D; + + public bool IsReadOnly => PKind is ParamKind.FloatReadOnlySpan or ParamKind.IntReadOnlySpan or ParamKind.DoubleReadOnlySpan or ParamKind.LongReadOnlySpan; + + public bool IsSpan => PKind is ParamKind.FloatSpan or ParamKind.FloatReadOnlySpan + or ParamKind.IntSpan or ParamKind.IntReadOnlySpan + or ParamKind.DoubleSpan or ParamKind.DoubleReadOnlySpan + or ParamKind.LongSpan or ParamKind.LongReadOnlySpan; + + public Kind ElemKind => PKind switch + { + ParamKind.IntArray or ParamKind.IntSpan or ParamKind.IntReadOnlySpan or ParamKind.IntArray2D => Kind.I, + ParamKind.DoubleArray or ParamKind.DoubleSpan or ParamKind.DoubleReadOnlySpan or ParamKind.DoubleArray2D => Kind.D, + ParamKind.LongArray or ParamKind.LongSpan or ParamKind.LongReadOnlySpan or ParamKind.LongArray2D => Kind.L, + _ => Kind.F, + }; + + /// + /// The flat 1-D span name used to view a 2-D array's row-major storage. + /// + public string FlatName => "__flat_" + Name; + + /// + /// The local holding the 2-D array's column count (GetLength(1)). + /// + public string ColsName => "__cols_" + Name; +} \ No newline at end of file diff --git a/src/IspcSharp.Generators/Models/ParamKind.cs b/src/IspcSharp.Generators/Models/ParamKind.cs new file mode 100644 index 0000000..5363e34 --- /dev/null +++ b/src/IspcSharp.Generators/Models/ParamKind.cs @@ -0,0 +1,11 @@ +namespace IspcSharp.Generators.Models; + +internal enum ParamKind +{ + FloatArray, FloatSpan, FloatReadOnlySpan, + IntArray, IntSpan, IntReadOnlySpan, + DoubleArray, DoubleSpan, DoubleReadOnlySpan, + LongArray, LongSpan, LongReadOnlySpan, + FloatArray2D, IntArray2D, DoubleArray2D, LongArray2D, + UniformFloat, UniformInt, UniformDouble, UniformLong, StructArray, Unsupported +} \ No newline at end of file diff --git a/src/IspcSharp.Generators/Models/ReduceOp.cs b/src/IspcSharp.Generators/Models/ReduceOp.cs new file mode 100644 index 0000000..2c2cab3 --- /dev/null +++ b/src/IspcSharp.Generators/Models/ReduceOp.cs @@ -0,0 +1,3 @@ +namespace IspcSharp.Generators.Models; + +internal enum ReduceOp { Add, Min, Max } \ No newline at end of file diff --git a/src/IspcSharp.Generators/Models/ReductionInfo.cs b/src/IspcSharp.Generators/Models/ReductionInfo.cs new file mode 100644 index 0000000..5d07217 --- /dev/null +++ b/src/IspcSharp.Generators/Models/ReductionInfo.cs @@ -0,0 +1,8 @@ +namespace IspcSharp.Generators.Models; + +internal sealed class ReductionInfo +{ + public string Name { get; set; } = ""; + public Kind LaneKind { get; set; } + public ReduceOp Op { get; set; } +} \ No newline at end of file diff --git a/src/IspcSharp.Generators/Models/StructField.cs b/src/IspcSharp.Generators/Models/StructField.cs new file mode 100644 index 0000000..87d55b4 --- /dev/null +++ b/src/IspcSharp.Generators/Models/StructField.cs @@ -0,0 +1,15 @@ +namespace IspcSharp.Generators.Models; + +/// +/// One field of a blittable struct. is 0 for a scalar field, +/// or the fixed element count for an ISPC-style array member ([SpmdArray(N)] float[] f). +/// +internal sealed record StructField(string Name, Kind Kind, int ArrayLength = 0) +{ + public bool IsArray => ArrayLength > 0; + + /// + /// Gang name of element of an array member (f_0, f_1, …). + /// + public string GangName(int i) => Name + "_" + i; +} diff --git a/src/IspcSharp.Generators/StructAndFunctionSupport.cs b/src/IspcSharp.Generators/Models/StructInfo.cs similarity index 69% rename from src/IspcSharp.Generators/StructAndFunctionSupport.cs rename to src/IspcSharp.Generators/Models/StructInfo.cs index aa6eb75..50e02aa 100644 --- a/src/IspcSharp.Generators/StructAndFunctionSupport.cs +++ b/src/IspcSharp.Generators/Models/StructInfo.cs @@ -3,36 +3,14 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace IspcSharp.Generators; - -/// -/// One field of a blittable struct. is 0 for a scalar field, -/// or the fixed element count for an ISPC-style array member ([SpmdArray(N)] float[] f). -/// -internal sealed class StructField(string name, SpmdGenerator.Kind kind, int arrayLength = 0) -{ - public readonly string Name = name; - public readonly SpmdGenerator.Kind Kind = kind; - public readonly int ArrayLength = arrayLength; - - public bool IsArray => ArrayLength > 0; - - /// - /// Gang name of element of an array member (f_0, f_1, …). - /// - public string GangName(int i) => Name + "_" + i; -} +namespace IspcSharp.Generators.Models; /// /// A [SpmdStruct] struct: name, namespace, and its primitive fields. Drives generation of /// the varying companion (Name__V, one gang-typed field each) and buffer gather/scatter. /// -internal sealed class StructInfo(string name, string ns, List fields) +internal sealed record StructInfo(string Name, string Namespace, EquatableReadOnlyList Fields) { - public readonly string Name = name; - public readonly string Namespace = ns; - public readonly List Fields = fields; - public string VName => Name + "__V"; /// @@ -47,9 +25,9 @@ internal sealed class StructInfo(string name, string ns, List field /// fields would break the uniform element stride, so those stay locals/args only. /// public bool AllFields32Bit => Fields.Count > 0 && - Fields.All(f => f.Kind is SpmdGenerator.Kind.F or SpmdGenerator.Kind.I); + Fields.All(f => f.Kind is Kind.F or Kind.I); - public SpmdGenerator.Kind FieldKind => Fields[0].Kind; + public Kind FieldKind => Fields[0].Kind; /// /// True when any field is an ISPC-style fixed-size array member. Such structs are @@ -61,7 +39,7 @@ internal sealed class StructInfo(string name, string ns, List field /// The companion's flattened gang fields: a scalar field yields one gang, an array /// member of length N yields N (f_0 … f_{N-1}). Drives companion emission and blends. /// - public IEnumerable<(string Name, SpmdGenerator.Kind Kind)> GangFields() + public IEnumerable<(string Name, Kind Kind)> GangFields() { foreach (var f in Fields) { @@ -116,7 +94,7 @@ internal sealed class StructInfo(string name, string ns, List field if (fields.Count == 0) return null; - return new StructInfo(s.Identifier.Text, NamespaceOf(s), fields); + return new StructInfo(s.Identifier.Text, NamespaceOf(s), new EquatableReadOnlyList(fields)); } /// @@ -146,12 +124,12 @@ private static int SpmdArrayLength(FieldDeclarationSyntax m) return 0; } - private static SpmdGenerator.Kind? KindOf(string t) => t switch + private static Kind? KindOf(string t) => t switch { - "float" => SpmdGenerator.Kind.F, - "int" => SpmdGenerator.Kind.I, - "double" => SpmdGenerator.Kind.D, - "long" => SpmdGenerator.Kind.L, + "float" => Kind.F, + "int" => Kind.I, + "double" => Kind.D, + "long" => Kind.L, _ => null, }; @@ -168,22 +146,3 @@ private static string NamespaceOf(SyntaxNode node) return ""; } } - -/// -/// A [SpmdFunction] helper: its declaration syntax and parsed signature. -/// -internal sealed class FunctionInfo(string name, MethodDeclarationSyntax syntax, List<(string Name, string Type)> ps, string ret) -{ - public readonly string Name = name; - public readonly MethodDeclarationSyntax Syntax = syntax; - public readonly List<(string Name, string Type)> Parameters = ps; - public readonly string ReturnType = ret; - - public static FunctionInfo From(MethodDeclarationSyntax m) - { - var ps = m.ParameterList.Parameters - .Select(p => (p.Identifier.Text, p.Type?.ToString().Trim() ?? "")) - .ToList(); - return new FunctionInfo(m.Identifier.Text, m, ps, m.ReturnType.ToString().Trim()); - } -} diff --git a/src/IspcSharp.Generators/Polyfills.cs b/src/IspcSharp.Generators/Polyfills.cs new file mode 100644 index 0000000..1a39516 --- /dev/null +++ b/src/IspcSharp.Generators/Polyfills.cs @@ -0,0 +1,4 @@ +#pragma warning disable IDE0130 +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit; diff --git a/src/IspcSharp.Generators/Rewriters/IdentifierRenameRewriter.cs b/src/IspcSharp.Generators/Rewriters/IdentifierRenameRewriter.cs new file mode 100644 index 0000000..79da09e --- /dev/null +++ b/src/IspcSharp.Generators/Rewriters/IdentifierRenameRewriter.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace IspcSharp.Generators.Rewriters; + +internal sealed class IdentifierRenameRewriter(HashSet names, string prefix) : CSharpSyntaxRewriter +{ + private readonly HashSet _names = names; + private readonly string _prefix = prefix; + + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) + => _names.Contains(node.Identifier.Text) + ? node.WithIdentifier(SyntaxFactory.Identifier(_prefix + node.Identifier.Text)) + : base.VisitIdentifierName(node); +} \ No newline at end of file diff --git a/src/IspcSharp.Generators/Rewriters/TwoDToFlatRewriter.cs b/src/IspcSharp.Generators/Rewriters/TwoDToFlatRewriter.cs new file mode 100644 index 0000000..431fb28 --- /dev/null +++ b/src/IspcSharp.Generators/Rewriters/TwoDToFlatRewriter.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace IspcSharp.Generators.Rewriters; + +internal sealed class TwoDToFlatRewriter(Dictionary map) : CSharpSyntaxRewriter +{ + private readonly Dictionary _map = map; + + public override SyntaxNode? VisitElementAccessExpression(ElementAccessExpressionSyntax node) + { + var visited = (ElementAccessExpressionSyntax)base.VisitElementAccessExpression(node)!; + if (visited.Expression is IdentifierNameSyntax id && + _map.TryGetValue(id.Identifier.Text, out var m) && + visited.ArgumentList.Arguments.Count == 2) + { + var row = visited.ArgumentList.Arguments[0].Expression; + var col = visited.ArgumentList.Arguments[1].Expression; + return SyntaxFactory.ParseExpression($"{m.Flat}[({row}) * {m.Cols} + ({col})]") + .WithTriviaFrom(visited); + } + + return visited; + } +} \ No newline at end of file diff --git a/src/IspcSharp.Generators/SpmdGenerator.cs b/src/IspcSharp.Generators/SpmdGenerator.cs index 1ffbc61..5e45f65 100644 --- a/src/IspcSharp.Generators/SpmdGenerator.cs +++ b/src/IspcSharp.Generators/SpmdGenerator.cs @@ -3,6 +3,10 @@ using System.Collections.Immutable; using System.Linq; using System.Text; +using IspcSharp.Generators.Contexts; +using IspcSharp.Generators.Exceptions; +using IspcSharp.Generators.Models; +using IspcSharp.Generators.Rewriters; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -27,112 +31,112 @@ namespace IspcSharp.Generators; public sealed class SpmdGenerator : IIncrementalGenerator { private const string AttributeFullName = "IspcSharp.SpmdAttribute"; - - private static readonly DiagnosticDescriptor NotPartial = new( - "ISPC001", "Containing type must be partial", - "[Spmd] method '{0}': the containing type must be declared 'partial' so generated code can be added", - "IspcSharp", DiagnosticSeverity.Error, true); - - private static readonly DiagnosticDescriptor BadShape = new( - "ISPC002", "Unsupported [Spmd] method shape", - "[Spmd] method '{0}': body must be [optional float/int/double locals], one 'foreach (var i in Spmd.Range(...))' or 'foreach (var (x, y) in Spmd.Range2D(...))' loop, then [optional trailing statements]", - "IspcSharp", DiagnosticSeverity.Error, true); - - private static readonly DiagnosticDescriptor Unsupported = new( - "ISPC003", "Construct not vectorizable", - "[Spmd] method '{0}': {1} is not supported by the SPMD vectorizer (v0.2 subset). Rewrite it, or use the IspcSharp runtime API directly for full control.", - "IspcSharp", DiagnosticSeverity.Error, true); - - private static readonly DiagnosticDescriptor BadParam = new( - "ISPC004", "Unsupported parameter type", - "[Spmd] method '{0}': parameter '{1}' has unsupported type '{2}'. Supported: float[]/int[]/double[], Span/ReadOnlySpan of float/int/double, and uniform float/int/double.", - "IspcSharp", DiagnosticSeverity.Error, true); - - private static readonly DiagnosticDescriptor NoParallel = new( - "ISPC005", "Parallel variant skipped", - "[Spmd] method '{0}': {0}_ParallelSimd was not generated: {1}", - "IspcSharp", DiagnosticSeverity.Info, true); - - internal static readonly DiagnosticDescriptor GatherPerf = new( - "ISPC101", "Gather (non-contiguous load) in SPMD kernel", - "'{0}' uses a lane-varying index, lowered to a per-lane gather (Memory.Gather), not a contiguous vector load. Make it contiguous (loop-variable/affine index, a transposed or SoA layout, or presorted indices) to avoid the gather.", - "IspcSharp.Performance", DiagnosticSeverity.Warning, true); - - internal static readonly DiagnosticDescriptor ScatterPerf = new( - "ISPC102", "Scatter (non-contiguous store) in SPMD kernel", - "'{0}' is a lane-varying indexed store, lowered to a per-active-lane scatter (Memory.Scatter); .NET has no hardware scatter instruction. Restructure to a contiguous write where possible.", - "IspcSharp.Performance", DiagnosticSeverity.Warning, true); - - internal static readonly DiagnosticDescriptor IntDividePerf = new( - "ISPC103", "Per-lane integer divide in SPMD kernel", - "integer '{0}' has no SIMD instruction and runs as a per-lane scalar loop. Expect scalar-ish throughput here; if the divisor is a constant power of two use a shift/mask instead.", - "IspcSharp.Performance", DiagnosticSeverity.Warning, true); - - internal static readonly DiagnosticDescriptor DoubleConvertPerf = new( - "ISPC104", "Double↔integer conversion in SPMD kernel", - "'{0}' converts between double and 64-bit integer lanes ((int)/(long) cast), which has no encoding before AVX-512DQ and runs per-lane on AVX2 (Zen 1–3, Haswell–Comet Lake). Keep the value in double, or move the conversion out of the hot loop.", - "IspcSharp.Performance", DiagnosticSeverity.Warning, true); + private const string StructAttributeFullName = "IspcSharp.SpmdStructAttribute"; + private const string FunctionAttributeFullName = "IspcSharp.SpmdFunctionAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { // Blittable [SpmdStruct] structs (parsed to name + primitive fields). - var structs = context.SyntaxProvider.CreateSyntaxProvider( - predicate: static (node, _) => node is StructDeclarationSyntax s && HasAttribute(s.AttributeLists, "SpmdStruct"), - transform: static (ctx, _) => StructInfo.From((StructDeclarationSyntax)ctx.Node)) + var structs = context.SyntaxProvider.ForAttributeWithMetadataName( + StructAttributeFullName, + predicate: static (node, _) => node is StructDeclarationSyntax, + transform: static (ctx, _) => StructInfo.From((StructDeclarationSyntax)ctx.TargetNode)) .Where(static s => s is not null) .Select(static (s, _) => s!) - .Collect(); + .WithTrackingName("SpmdStructs"); // [SpmdFunction] helpers. - var functions = context.SyntaxProvider.CreateSyntaxProvider( - predicate: static (node, _) => node is MethodDeclarationSyntax m && HasAttribute(m.AttributeLists, "SpmdFunction"), - transform: static (ctx, _) => FunctionInfo.From((MethodDeclarationSyntax)ctx.Node)) - .Collect(); + var functions = context.SyntaxProvider.ForAttributeWithMetadataName( + FunctionAttributeFullName, + predicate: static (node, _) => node is MethodDeclarationSyntax, + transform: static (ctx, _) => FunctionInfo.From((MethodDeclarationSyntax)ctx.TargetNode)) + .WithTrackingName("SpmdFunctions"); - var methods = context.SyntaxProvider.ForAttributeWithMetadataName( + // [Spmd] kernels. + var kernels = context.SyntaxProvider.ForAttributeWithMetadataName( AttributeFullName, predicate: static (node, _) => node is MethodDeclarationSyntax, - transform: static (ctx, _) => (MethodDeclarationSyntax)ctx.TargetNode); + transform: static (ctx, _) => KernelInfo.From((MethodDeclarationSyntax)ctx.TargetNode)) + .WithTrackingName("SpmdKernels"); + + var structTable = structs.Collect() + .Select(static (arr, _) => new EquatableReadOnlyList( + [.. arr.OrderBy(s => s.Name, StringComparer.Ordinal)])) + .WithTrackingName("SpmdStructTable"); + var functionTable = functions.Collect() + .Select(static (arr, _) => new EquatableReadOnlyList( + [.. arr.OrderBy(f => f.Name, StringComparer.Ordinal)])) + .WithTrackingName("SpmdFunctionTable"); - var tables = structs.Combine(functions); + var tables = structTable.Combine(functionTable); // Varying companions for the blittable structs (one source file for all of them). - context.RegisterSourceOutput(structs, static (spc, all) => EmitStructCompanions(spc, all)); + context.RegisterSourceOutput(structTable, static (spc, all) => + { + string? src = EmitStructCompanions(all); + if (src != null) + spc.AddSource("__SpmdStructs.g.cs", SourceText.From(src, Encoding.UTF8)); + }); // Varying companions for the [SpmdFunction] helpers. - context.RegisterSourceOutput(functions.Combine(tables), static (spc, pair) => + context.RegisterSourceOutput(tables, static (spc, pair) => { - foreach (var fn in pair.Left) + var structMap = BuildStructMap(pair.Left); + var fnMap = BuildFunctionMap(pair.Right); + foreach (var fn in fnMap.Values) { try { - EmitFunctionCompanion(spc, fn, pair.Right.Left, pair.Right.Right); + var method = ParseMethod(fn.DeclarationText); + if (method is null) + continue; + string? src = GenerateFunctionSource(fn, method, structMap, fnMap, DropDiagnostic); + if (src != null) + spc.AddSource($"{fn.Name}_SpmdFn.g.cs", SourceText.From(src, Encoding.UTF8)); } - catch (UnsupportedConstructException ex) + catch (UnsupportedConstructException) { - spc.ReportDiagnostic(Diagnostic.Create( - Unsupported, ex.Location ?? fn.Syntax.GetLocation(), fn.Name, ex.What)); + // Reported (with a real location) by SpmdDiagnosticsAnalyzer. } } }); // [Spmd] kernels, with access to the struct/function tables. - context.RegisterSourceOutput(methods.Combine(tables), static (spc, pair) => + context.RegisterSourceOutput(kernels.Combine(tables), static (spc, pair) => { - var method = pair.Left; + var kernel = pair.Left; try { - Generate(spc, method, pair.Right.Left, pair.Right.Right); + var method = ParseMethod(kernel.MethodText); + if (method is null) + return; + var structMap = BuildStructMap(pair.Right.Left); + var fnMap = BuildFunctionMap(pair.Right.Right); + string? src = GenerateKernelSource(kernel, method, structMap, fnMap, DropDiagnostic); + if (src != null) + spc.AddSource($"{kernel.Name}_Spmd.g.cs", SourceText.From(src, Encoding.UTF8)); } - catch (UnsupportedConstructException ex) + catch (UnsupportedConstructException) { - spc.ReportDiagnostic(Diagnostic.Create( - Unsupported, ex.Location ?? method.GetLocation(), - method.Identifier.Text, ex.What)); + // Reported (with a real location) by SpmdDiagnosticsAnalyzer. } }); } + /// + /// Diagnostic sink for generator-side runs: the analyzer owns reporting, so the + /// generation pass discards what the engine surfaces. + /// + private static void DropDiagnostic(Diagnostic _) + { + } + + /// + /// Re-parse a pipeline model's declaration text back to a method node for emission. + /// + private static MethodDeclarationSyntax? ParseMethod(string declarationText) + => SyntaxFactory.ParseMemberDeclaration(declarationText) as MethodDeclarationSyntax; + /// /// Read a bool named argument from the method's [Spmd(...)] attribute. /// @@ -162,7 +166,7 @@ private static bool GetSpmdBoolArg(MethodDeclarationSyntax method, string argNam /// /// Syntactic attribute check (matches "Name" or "NameAttribute"). /// - private static bool HasAttribute(SyntaxList lists, string name) + internal static bool HasAttribute(SyntaxList lists, string name) { foreach (var list in lists) { @@ -180,94 +184,23 @@ private static bool HasAttribute(SyntaxList lists, string n return false; } - internal enum Kind { F, I, D, L } - internal enum ParamKind - { - FloatArray, FloatSpan, FloatReadOnlySpan, - IntArray, IntSpan, IntReadOnlySpan, - DoubleArray, DoubleSpan, DoubleReadOnlySpan, - LongArray, LongSpan, LongReadOnlySpan, - FloatArray2D, IntArray2D, DoubleArray2D, LongArray2D, - UniformFloat, UniformInt, UniformDouble, UniformLong, StructArray, Unsupported - } - - internal readonly struct ParamInfo(string name, ParamKind kind, string typeText, string? structType = null) - { - public readonly string Name = name; - public readonly ParamKind PKind = kind; - public readonly string TypeText = typeText; - - /// - /// Element struct type name for a parameter. - /// - public readonly string? StructType = structType; - - public bool IsStructBuffer => PKind == ParamKind.StructArray; - - public bool Is2D => PKind is ParamKind.FloatArray2D or ParamKind.IntArray2D or ParamKind.DoubleArray2D or ParamKind.LongArray2D; - - public bool IsBuffer => PKind is ParamKind.FloatArray or ParamKind.FloatSpan or ParamKind.FloatReadOnlySpan - or ParamKind.IntArray or ParamKind.IntSpan or ParamKind.IntReadOnlySpan - or ParamKind.DoubleArray or ParamKind.DoubleSpan or ParamKind.DoubleReadOnlySpan - or ParamKind.LongArray or ParamKind.LongSpan or ParamKind.LongReadOnlySpan - or ParamKind.FloatArray2D or ParamKind.IntArray2D or ParamKind.DoubleArray2D or ParamKind.LongArray2D; - - public bool IsReadOnly => PKind is ParamKind.FloatReadOnlySpan or ParamKind.IntReadOnlySpan or ParamKind.DoubleReadOnlySpan or ParamKind.LongReadOnlySpan; - - public bool IsSpan => PKind is ParamKind.FloatSpan or ParamKind.FloatReadOnlySpan - or ParamKind.IntSpan or ParamKind.IntReadOnlySpan - or ParamKind.DoubleSpan or ParamKind.DoubleReadOnlySpan - or ParamKind.LongSpan or ParamKind.LongReadOnlySpan; - - public Kind ElemKind => PKind switch - { - ParamKind.IntArray or ParamKind.IntSpan or ParamKind.IntReadOnlySpan or ParamKind.IntArray2D => Kind.I, - ParamKind.DoubleArray or ParamKind.DoubleSpan or ParamKind.DoubleReadOnlySpan or ParamKind.DoubleArray2D => Kind.D, - ParamKind.LongArray or ParamKind.LongSpan or ParamKind.LongReadOnlySpan or ParamKind.LongArray2D => Kind.L, - _ => Kind.F, - }; - - /// - /// The flat 1-D span name used to view a 2-D array's row-major storage. - /// - public string FlatName => "__flat_" + Name; - - /// - /// The local holding the 2-D array's column count (GetLength(1)). - /// - public string ColsName => "__cols_" + Name; - } - - internal enum ReduceOp { Add, Min, Max } - - internal sealed class ReductionInfo - { - public string Name { get; set; } = ""; - public Kind LaneKind { get; set; } - public ReduceOp Op { get; set; } - } - - internal sealed class UnsupportedConstructException(string what, Location? loc = null) : Exception - { - public string What { get; } = what; - public Location? Location { get; } = loc; - } - - private static void Generate(SourceProductionContext spc, MethodDeclarationSyntax method, - ImmutableArray structsArr, ImmutableArray functionsArr) + /// + /// The shared kernel engine: validates the [Spmd] method and emits its vectorized + /// companion source, surfacing diagnostics through . The + /// generator calls this with a discarding sink; SpmdDiagnosticsAnalyzer calls it with + /// the real method syntax and reports what it surfaces. + /// + internal static string? GenerateKernelSource(KernelInfo kernel, MethodDeclarationSyntax method, + Dictionary structMap, Dictionary fnMap, + Action report) { - string name = method.Identifier.Text; + string name = kernel.Name; var location = method.GetLocation(); - var structMap = BuildStructMap(structsArr); - var fnMap = functionsArr.IsDefaultOrEmpty - ? [] - : functionsArr.GroupBy(f => f.Name).ToDictionary(g => g.Key, g => g.First()); - if (method.Parent is not TypeDeclarationSyntax type || - !type.Modifiers.Any(SyntaxKind.PartialKeyword)) + if (!kernel.TypeIsPartial) { - spc.ReportDiagnostic(Diagnostic.Create(NotPartial, location, name)); - return; + report(Diagnostic.Create(Descriptors.NotPartial, location, name)); + return null; } var paramInfos = new List(); @@ -327,8 +260,8 @@ private static void Generate(SourceProductionContext spc, MethodDeclarationSynta if (kind == ParamKind.Unsupported) { - spc.ReportDiagnostic(Diagnostic.Create(BadParam, p.GetLocation(), name, p.Identifier.Text, t)); - return; + report(Diagnostic.Create(Descriptors.BadParam, p.GetLocation(), name, p.Identifier.Text, t)); + return null; } paramInfos.Add(new ParamInfo(p.Identifier.Text, kind, p.Type!.ToString(), structElem)); @@ -336,8 +269,8 @@ private static void Generate(SourceProductionContext spc, MethodDeclarationSynta if (method.Body is null) { - spc.ReportDiagnostic(Diagnostic.Create(BadShape, location, name)); - return; + report(Diagnostic.Create(Descriptors.BadShape, location, name)); + return null; } // Find the single Spmd.Range* loop, it may be nested inside uniform control flow @@ -347,8 +280,8 @@ private static void Generate(SourceProductionContext spc, MethodDeclarationSynta .ToList(); if (spmdForeaches.Count == 0) { - spc.ReportDiagnostic(Diagnostic.Create(BadShape, location, name)); - return; + report(Diagnostic.Create(Descriptors.BadShape, location, name)); + return null; } if (spmdForeaches.Count > 1) @@ -432,8 +365,8 @@ pvd.Variables[0] is SingleVariableDesignationSyntax xd && } else { - spc.ReportDiagnostic(Diagnostic.Create(BadShape, location, name)); - return; + report(Diagnostic.Create(Descriptors.BadShape, location, name)); + return null; } string returnType = method.ReturnType.ToString(); @@ -535,7 +468,7 @@ pvd.Variables[0] is SingleVariableDesignationSyntax xd && var emitter = new VectorBodyEmitter(loopVar, paramInfos, uniformPreLocals, preLocals, reductions, doubleMode, longMode, structMap, fnMap, streaming); string vectorBody = emitter.EmitStatements(body, maskExpr: null, indent: " "); foreach (var diag in emitter.Diagnostics) - spc.ReportDiagnostic(diag); + report(diag); // Per-lane 'return' retires one element. In the scalar tail that must advance to // the NEXT element (not exit the method), so bare returns become 'goto __tail_next;' @@ -553,11 +486,12 @@ pvd.Variables[0] is SingleVariableDesignationSyntax xd && bool hasSpanParams = paramInfos.Any(p => p.IsSpan); string paramDecl = string.Join(", ", paramInfos.Select(p => $"{p.TypeText} {p.Name}")); string paramPass = string.Join(", ", paramInfos.Select(p => p.Name)); - string ns = GetNamespace(type); - string typeHeader = $"{type.Modifiers} {type.Keyword.Text} {type.Identifier.Text}"; + string ns = kernel.Namespace; + string typeHeader = kernel.TypeHeader; var src = new StringBuilder(); _ = src.AppendLine("// "); + _ = src.AppendLine("#pragma warning disable 1591, 0419 // generated companions carry no full xmldoc"); _ = src.AppendLine("using System;"); _ = src.AppendLine("using IspcSharp;"); _ = src.AppendLine(); @@ -634,22 +568,22 @@ pvd.Variables[0] is SingleVariableDesignationSyntax xd && bool wantParallel = WantsParallel(method); if (!simpleShape) { - spc.ReportDiagnostic(Diagnostic.Create(NoParallel, location, name, + report(Diagnostic.Create(Descriptors.NoParallel, location, name, "the vectorized loop is nested in uniform control flow; only _Simd is generated (parallelize the outer loop yourself)")); } else if (paramInfos.Any(p => p.Is2D)) { - spc.ReportDiagnostic(Diagnostic.Create(NoParallel, location, name, + report(Diagnostic.Create(Descriptors.NoParallel, location, name, "2D-array buffers use a flat-span view that cannot be captured across threads; only _Simd is generated")); } else if (paramInfos.Any(p => p.IsStructBuffer)) { - spc.ReportDiagnostic(Diagnostic.Create(NoParallel, location, name, + report(Diagnostic.Create(Descriptors.NoParallel, location, name, "struct buffers use a flat-span view that cannot be captured across threads; only _Simd is generated (parallelize the outer loop yourself)")); } else if (wantParallel && hasSpanParams) { - spc.ReportDiagnostic(Diagnostic.Create(NoParallel, location, name, + report(Diagnostic.Create(Descriptors.NoParallel, location, name, "Span parameters cannot be captured across threads; use float[]/int[]")); } else if (wantParallel) @@ -773,7 +707,7 @@ pvd.Variables[0] is SingleVariableDesignationSyntax xd && if (ns.Length > 0) _ = src.AppendLine("}"); - spc.AddSource($"{name}_Spmd.g.cs", SourceText.From(src.ToString(), Encoding.UTF8)); + return src.ToString(); } private static void EmitLoopCore(StringBuilder src, string ind, string startExpr, string endExpr, @@ -936,14 +870,15 @@ internal static string VaryingTypeOf(string scalarType, IReadOnlyDictionary /// Emit the varying companion struct (Name__V) for every blittable struct. /// - private static void EmitStructCompanions(SourceProductionContext spc, ImmutableArray structs) + internal static string? EmitStructCompanions(IReadOnlyList structs) { - if (structs.IsDefaultOrEmpty) - return; + if (structs.Count == 0) + return null; var byName = structs.GroupBy(s => s.Name).Select(g => g.First()).ToList(); var src = new StringBuilder(); _ = src.AppendLine("// "); + _ = src.AppendLine("#pragma warning disable 1591, 0419 // generated companions carry no full xmldoc"); _ = src.AppendLine("using System;"); _ = src.AppendLine("using IspcSharp;"); _ = src.AppendLine(); @@ -986,26 +921,24 @@ private static void EmitStructCompanions(SourceProductionContext spc, ImmutableA _ = src.AppendLine("}"); } - spc.AddSource("__SpmdStructs.g.cs", SourceText.From(src.ToString(), Encoding.UTF8)); + return src.ToString(); } /// - /// Emit the varying companion of one [SpmdFunction] helper. + /// Emit the varying companion of one [SpmdFunction] helper. Same dual-caller + /// contract as : the generator discards diagnostics, + /// the analyzer reports them. /// - private static void EmitFunctionCompanion(SourceProductionContext spc, FunctionInfo fn, - ImmutableArray structs, ImmutableArray functions) + internal static string? GenerateFunctionSource(FunctionInfo fn, MethodDeclarationSyntax method, + Dictionary structMap, Dictionary fnMap, + Action report) { - var method = fn.Syntax; - if (method.Parent is not TypeDeclarationSyntax type || - !type.Modifiers.Any(SyntaxKind.PartialKeyword)) + if (!fn.TypeIsPartial) { - spc.ReportDiagnostic(Diagnostic.Create(NotPartial, method.GetLocation(), fn.Name)); - return; + report(Diagnostic.Create(Descriptors.NotPartial, method.GetLocation(), fn.Name)); + return null; } - var structMap = BuildStructMap(structs); - var fnMap = functions.GroupBy(f => f.Name).ToDictionary(g => g.Key, g => g.First()); - if (method.Body is null && method.ExpressionBody is null) throw new UnsupportedConstructException("[SpmdFunction] with no body", method.GetLocation()); @@ -1030,11 +963,12 @@ private static void EmitFunctionCompanion(SourceProductionContext spc, FunctionI var emitter = new VectorBodyEmitter(structMap, fnMap, paramLocals, paramStructs); string body = emitter.EmitFunctionBody(method, " "); foreach (var diag in emitter.Diagnostics) - spc.ReportDiagnostic(diag); + report(diag); - string ns = GetNamespace(type); + string ns = fn.Namespace; var src = new StringBuilder(); _ = src.AppendLine("// "); + _ = src.AppendLine("#pragma warning disable 1591, 0419 // generated companions carry no full xmldoc"); _ = src.AppendLine("using System;"); _ = src.AppendLine("using IspcSharp;"); _ = src.AppendLine(); @@ -1044,7 +978,7 @@ private static void EmitFunctionCompanion(SourceProductionContext spc, FunctionI _ = src.AppendLine("{"); } - _ = src.AppendLine($"{type.Modifiers} {type.Keyword.Text} {type.Identifier.Text}"); + _ = src.AppendLine(fn.TypeHeader); _ = src.AppendLine("{"); _ = src.AppendLine($" /// Varying companion of (generated)."); _ = src.AppendLine($" public static {vret} {fn.Name}({vparams})"); @@ -1055,13 +989,14 @@ private static void EmitFunctionCompanion(SourceProductionContext spc, FunctionI if (ns.Length > 0) _ = src.AppendLine("}"); - spc.AddSource($"{fn.Name}_SpmdFn.g.cs", SourceText.From(src.ToString(), Encoding.UTF8)); + return src.ToString(); } - internal static Dictionary BuildStructMap(ImmutableArray structs) - => structs.IsDefaultOrEmpty - ? [] - : structs.GroupBy(s => s.Name).ToDictionary(g => g.Key, g => g.First()); + internal static Dictionary BuildStructMap(IEnumerable structs) + => structs.GroupBy(s => s.Name).ToDictionary(g => g.Key, g => g.First()); + + internal static Dictionary BuildFunctionMap(IEnumerable functions) + => functions.GroupBy(f => f.Name).ToDictionary(g => g.Key, g => g.First()); internal static Kind KindOfScalar(string t, Location loc) => t switch { @@ -1287,18 +1222,6 @@ private static bool WantsParallel(MethodDeclarationSyntax method) return true; } - private static string GetNamespace(SyntaxNode node) - { - var parts = new List(); - for (var n = node.Parent; n != null; n = n.Parent) - { - if (n is BaseNamespaceDeclarationSyntax nds) - parts.Insert(0, nds.Name.ToString()); - } - - return string.Join(".", parts); - } - private static string Reindent(SyntaxList statements, string indent) { var sb = new StringBuilder(); @@ -1386,27 +1309,6 @@ private static SyntaxList Rewrite2DToFlat(SyntaxList (StatementSyntax)rewriter.Visit(s))); } - private sealed class TwoDToFlatRewriter(Dictionary map) : CSharpSyntaxRewriter - { - private readonly Dictionary _map = map; - - public override SyntaxNode? VisitElementAccessExpression(ElementAccessExpressionSyntax node) - { - var visited = (ElementAccessExpressionSyntax)base.VisitElementAccessExpression(node)!; - if (visited.Expression is IdentifierNameSyntax id && - _map.TryGetValue(id.Identifier.Text, out var m) && - visited.ArgumentList.Arguments.Count == 2) - { - var row = visited.ArgumentList.Arguments[0].Expression; - var col = visited.ArgumentList.Arguments[1].Expression; - return SyntaxFactory.ParseExpression($"{m.Flat}[({row}) * {m.Cols} + ({col})]") - .WithTriviaFrom(visited); - } - - return visited; - } - } - /// /// Rewrite every occurrence of the given identifiers to '{prefix}{name}'. /// @@ -1417,17 +1319,6 @@ private static SyntaxList RenameIdentifiers( return SyntaxFactory.List(statements.Select(s => (StatementSyntax)rewriter.Visit(s))); } - private sealed class IdentifierRenameRewriter(HashSet names, string prefix) : CSharpSyntaxRewriter - { - private readonly HashSet _names = names; - private readonly string _prefix = prefix; - - public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) - => _names.Contains(node.Identifier.Text) - ? node.WithIdentifier(SyntaxFactory.Identifier(_prefix + node.Identifier.Text)) - : base.VisitIdentifierName(node); - } - private static bool IsSpmdRangeCallee(InvocationExpressionSyntax inv) { string c = inv.Expression.ToString().Replace(" ", ""); @@ -1497,34 +1388,6 @@ private static void EmitFlatSpanSetup(StringBuilder src, string indent, List reductions, - string laneCountExpr, - bool doubleMode, - bool longMode, - int unroll, - bool hasLaneReturns) - { - public readonly CommonForEachStatementSyntax Fe = fe; - public readonly string LoopVar = loopVar; - public readonly string StartExpr = startExpr; - public readonly string EndExpr = endExpr; - public readonly string VectorBody = vectorBody; - public readonly string ScalarBody = scalarBody; - public readonly string LaneCountExpr = laneCountExpr; - public readonly List Reductions = reductions; - public readonly bool DoubleMode = doubleMode; - public readonly bool LongMode = longMode; - public readonly bool HasLaneReturns = hasLaneReturns; - public readonly int Unroll = unroll; - } - private static void EmitScaffold(StringBuilder src, IEnumerable statements, string indent, ScaffoldContext ctx) { foreach (var st in statements) diff --git a/src/IspcSharp.Generators/VectorBodyEmitter.cs b/src/IspcSharp.Generators/VectorBodyEmitter.cs index 46c2565..2ce6212 100644 --- a/src/IspcSharp.Generators/VectorBodyEmitter.cs +++ b/src/IspcSharp.Generators/VectorBodyEmitter.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using IspcSharp.Generators.Contexts; +using IspcSharp.Generators.Exceptions; +using IspcSharp.Generators.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -24,10 +27,10 @@ namespace IspcSharp.Generators; internal sealed class VectorBodyEmitter { private readonly string _loopVar; - private readonly Dictionary _params; + private readonly Dictionary _params; private readonly HashSet _uniformPreLocals; - private readonly Dictionary _preLocalKinds; - private readonly Dictionary _locals = []; + private readonly Dictionary _preLocalKinds; + private readonly Dictionary _locals = []; /// /// Int lane locals that are affine functions of the loop variable with unit stride: @@ -37,7 +40,7 @@ internal sealed class VectorBodyEmitter /// private readonly Dictionary _affineOffset = []; - private readonly Dictionary _reductions; + private readonly Dictionary _reductions; private readonly bool _d; // double-lane kernel (VDouble/VMaskD gangs) private readonly bool _l; // long-lane kernel (VLong/VMaskD gangs) private readonly bool _streaming; // emit non-temporal stores ([Spmd(Streaming=true)]) @@ -50,7 +53,7 @@ internal sealed class VectorBodyEmitter private readonly IReadOnlyDictionary _structs; private readonly IReadOnlyDictionary _functions; private readonly Dictionary _structLocals = []; - private SpmdGenerator.Kind _retKind; + private Kind _retKind; private string? _retStruct; private int _maskCounter; private readonly Stack _loopStack = new(); @@ -71,17 +74,17 @@ private void Warn(DiagnosticDescriptor d, SyntaxNode at, string arg) private string MT => Wide ? "VMaskD" : "VMask"; private string MTAll => Wide ? "VMaskD.All" : "VMask.All"; private string MTNone => Wide ? "VMaskD.None" : "VMask.None"; - private SpmdGenerator.Kind LaneFloatKind => _l ? SpmdGenerator.Kind.L : _d ? SpmdGenerator.Kind.D : SpmdGenerator.Kind.F; + private Kind LaneFloatKind => _l ? Kind.L : _d ? Kind.D : Kind.F; private string IntT => _l ? "VLong" : "VInt"; - private string VType(SpmdGenerator.Kind k) => SpmdGenerator.VType(k, _d, _l); - private string SelectOf(SpmdGenerator.Kind k) => SpmdGenerator.VType(k, _d, _l) + ".Select"; + private string VType(Kind k) => SpmdGenerator.VType(k, _d, _l); + private string SelectOf(Kind k) => SpmdGenerator.VType(k, _d, _l) + ".Select"; public VectorBodyEmitter( string loopVar, - List parameters, + List parameters, HashSet uniformPreLocals, - Dictionary preLocalKinds, - List reductions, + Dictionary preLocalKinds, + List reductions, bool doubleMode, bool longMode, IReadOnlyDictionary structs, @@ -111,7 +114,7 @@ public VectorBodyEmitter( public VectorBodyEmitter( IReadOnlyDictionary structs, IReadOnlyDictionary functions, - Dictionary paramLocals, + Dictionary paramLocals, Dictionary paramStructs) { _loopVar = ""; @@ -168,7 +171,7 @@ private void EmitFunctionStatement(StatementSyntax st, string indent, StringBuil EmitFunctionStatement(inner, indent, sb); return; default: - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"statement '{Trunc(st)}' in a [SpmdFunction] (use locals, assignments, and a final 'return expr')", st.GetLocation()); } } @@ -309,7 +312,7 @@ private void EmitStatement(StatementSyntax st, string? mask, string indent, Stri break; default: - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"statement '{Trunc(st)}' ({st.Kind()})", st.GetLocation()); } } @@ -363,25 +366,25 @@ private void EmitLocalDecl(LocalDeclarationStatementSyntax decl, string? mask, s var kind = t switch { - "float" => SpmdGenerator.Kind.F, - "int" => SpmdGenerator.Kind.I, - "double" => SpmdGenerator.Kind.D, - "long" => SpmdGenerator.Kind.L, - "var" => throw new SpmdGenerator.UnsupportedConstructException( + "float" => Kind.F, + "int" => Kind.I, + "double" => Kind.D, + "long" => Kind.L, + "var" => throw new UnsupportedConstructException( "'var' local (declare as float, int, double, long, or a [SpmdStruct] type)", decl.GetLocation()), - _ => throw new SpmdGenerator.UnsupportedConstructException( + _ => throw new UnsupportedConstructException( $"local of type '{t}' (only float/int/double/long and [SpmdStruct] locals)", decl.GetLocation()), }; - if (_l && kind != SpmdGenerator.Kind.L) + if (_l && kind != Kind.L) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"'{t}' lane local in a long kernel (64-bit integer gang; declare it as long)", decl.GetLocation()); } - if (_d && kind != SpmdGenerator.Kind.D) + if (_d && kind != Kind.D) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"'{t}' lane local in a double kernel (double gangs have a different lane count; declare it as double)", decl.GetLocation()); } @@ -400,7 +403,7 @@ private void EmitLocalDecl(LocalDeclarationStatementSyntax decl, string? mask, s // Record affine-in-lane int locals declared unmasked, so uses as array indices // become contiguous loads/stores. (In a masked branch the initializer is blended // with 0 for inactive lanes, which breaks the affine property, so skip those.) - if (kind is SpmdGenerator.Kind.I or SpmdGenerator.Kind.L && mask == null && + if (kind is Kind.I or Kind.L && mask == null && v.Initializer != null && TryClassifyAffine(v.Initializer.Value, out string? aoff)) { _affineOffset[v.Identifier.Text] = aoff; @@ -477,7 +480,7 @@ private void EmitAssignment(AssignmentExpressionSyntax asg, string? mask, string if (asg.Left is IdentifierNameSyntax sid && _structLocals.TryGetValue(sid.Identifier.Text, out string? sst)) { if (op != "=") - throw new SpmdGenerator.UnsupportedConstructException($"compound '{op}' on struct '{sid.Identifier.Text}'", asg.GetLocation()); + throw new UnsupportedConstructException($"compound '{op}' on struct '{sid.Identifier.Text}'", asg.GetLocation()); string rhs = VecStruct(asg.Right).Code; _ = sb.AppendLine(mask == null ? $"{indent}{sid.Identifier.Text} = {rhs};" @@ -520,9 +523,9 @@ private void EmitAssignment(AssignmentExpressionSyntax asg, string? mask, string if (asg.Left is ElementAccessExpressionSyntax sea && TryStructBufferField(sea, out string? sbuf, out string? sbs, out string? sbidx)) { if (op != "=") - throw new SpmdGenerator.UnsupportedConstructException($"compound '{op}' on struct-buffer element '{sbuf}[...]'", asg.GetLocation()); + throw new UnsupportedConstructException($"compound '{op}' on struct-buffer element '{sbuf}[...]'", asg.GetLocation()); if (_structs[sbs].Fields.Count > 1) - Warn(SpmdGenerator.ScatterPerf, sea, sea.ToString()); + Warn(Descriptors.ScatterPerf, sea, sea.ToString()); string tmp = $"__sv{_maskCounter++}"; _ = sb.AppendLine($"{indent}var {tmp} = {VecStruct(asg.Right).Code};"); foreach (var f in _structs[sbs].Fields) @@ -536,11 +539,11 @@ private void EmitAssignment(AssignmentExpressionSyntax asg, string? mask, string var rkind = rinfo.LaneKind; string sel = SelectOf(rkind); - if (rinfo.Op == SpmdGenerator.ReduceOp.Add) + if (rinfo.Op == ReduceOp.Add) { if (op != "+=") { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"reduction '{rname}' with operator '{op}' (this accumulator uses '+=')", asg.GetLocation()); } // 'acc += x * y' on float/double lanes fuses into one FMA (matmul / dot product / @@ -567,7 +570,7 @@ asg.Right is not InvocationExpressionSyntax minMaxCall || !SpmdGenerator.IsMinMaxSelfCall(minMaxCall, rname, out var callOp) || callOp != rinfo.Op) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"reduction '{rname}' accumulation (this accumulator uses '{rname} = Math.{rinfo.Op}({rname}, ...)')", asg.GetLocation()); } @@ -575,7 +578,7 @@ asg.Right is not InvocationExpressionSyntax minMaxCall || .First(a => a.Expression is not IdentifierNameSyntax id || id.Identifier.Text != rname) .Expression; string val = Coerce(Vec(otherArg), rkind, otherArg); - string fn = rinfo.Op == SpmdGenerator.ReduceOp.Min ? "Min" : "Max"; + string fn = rinfo.Op == ReduceOp.Min ? "Min" : "Max"; string identity = SpmdGenerator.VectorIdentity(rinfo, _d, _l); _ = sb.AppendLine(mask == null ? $"{indent}__red_{rname} = VectorMath.{fn}(__red_{rname}, {val});" @@ -588,7 +591,7 @@ asg.Right is not InvocationExpressionSyntax minMaxCall || { string name = id.Identifier.Text; // Reassignment can change (or destroy) the affine-in-lane property. - if (lkind == SpmdGenerator.Kind.I && op == "=" && mask == null && + if (lkind == Kind.I && op == "=" && mask == null && TryClassifyAffine(asg.Right, out string? aoff)) { _affineOffset[name] = aoff; @@ -618,9 +621,9 @@ ea2d.Expression is IdentifierNameSyntax a2d && if (asg.Left is ElementAccessExpressionSyntax ea && TryGetContiguousIndex(ea, out string? bufName, out string? storeIdx)) { if (!_params.TryGetValue(bufName, out var p) || !p.IsBuffer) - throw new SpmdGenerator.UnsupportedConstructException($"store to unknown buffer '{bufName}'", asg.GetLocation()); + throw new UnsupportedConstructException($"store to unknown buffer '{bufName}'", asg.GetLocation()); if (p.IsReadOnly) - throw new SpmdGenerator.UnsupportedConstructException($"store to ReadOnlySpan '{bufName}'", asg.GetLocation()); + throw new UnsupportedConstructException($"store to ReadOnlySpan '{bufName}'", asg.GetLocation()); var ek = p.ElemKind; string vtype = VType(ek); @@ -628,7 +631,7 @@ ea2d.Expression is IdentifierNameSyntax a2d && string rhs = CombineCompoundOnExpr(op, load, asg, ek); // Streaming: a full-gang, non-compound contiguous write to a float/double buffer can use a // non-temporal store (cache-bypassing). Masked/compound/other paths keep the ordinary store. - string store = _streaming && op == "=" && ek is SpmdGenerator.Kind.F or SpmdGenerator.Kind.D + string store = _streaming && op == "=" && ek is Kind.F or Kind.D ? "StoreNonTemporal" : "Store"; _ = mask == null ? sb.AppendLine($"{indent}({rhs}).{store}({bufName}, {storeIdx});") @@ -643,33 +646,33 @@ eaScatter.Expression is IdentifierNameSyntax scatterId && { if (scatterParam.IsReadOnly) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"scatter to ReadOnlySpan '{scatterId.Identifier.Text}'", asg.GetLocation()); } if (op != "=") { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"compound scatter '{op}' on '{scatterId.Identifier.Text}' (only '=' supported for indexed stores)", asg.GetLocation()); } - Warn(SpmdGenerator.ScatterPerf, eaScatter, eaScatter.ToString()); + Warn(Descriptors.ScatterPerf, eaScatter, eaScatter.ToString()); // Lower: Memory.Scatter(buf, indices, values, mask). // Long kernels index with VLong directly; double kernels truncate to VLong. var ek = scatterParam.ElemKind; var idxExpr = eaScatter.ArgumentList.Arguments[0].Expression; string val = Coerce(Vec(asg.Right), ek, asg.Right); string idx = _l - ? Coerce(Vec(idxExpr), SpmdGenerator.Kind.L, idxExpr) + ? Coerce(Vec(idxExpr), Kind.L, idxExpr) : _d - ? $"VLong.FromDoubleTruncate({Coerce(Vec(idxExpr), SpmdGenerator.Kind.D, idxExpr)})" - : Coerce(Vec(idxExpr), SpmdGenerator.Kind.I, idxExpr); + ? $"VLong.FromDoubleTruncate({Coerce(Vec(idxExpr), Kind.D, idxExpr)})" + : Coerce(Vec(idxExpr), Kind.I, idxExpr); string maskArg = mask ?? MTAll; _ = sb.AppendLine($"{indent}Memory.Scatter({scatterId.Identifier.Text}, {idx}, {val}, {maskArg});"); return; } - throw new SpmdGenerator.UnsupportedConstructException($"assignment target '{Trunc(asg.Left)}'", asg.GetLocation()); + throw new UnsupportedConstructException($"assignment target '{Trunc(asg.Left)}'", asg.GetLocation()); } /// @@ -685,14 +688,14 @@ private void EmitReturn(ReturnStatementSyntax rs, string? mask, string indent, S { if (rs.Expression != null) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( "'return ' inside the loop (value returns belong after the loop; use a bare 'return;' to retire the lane)", rs.GetLocation()); } if (_loopStack.Any(ctx => ctx.IsUniform)) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( "'return' inside a uniform for loop (per-lane returns can't exit a shared scalar loop; use a varying loop or restructure)", rs.GetLocation()); } @@ -709,7 +712,7 @@ private void EmitIncDec(ExpressionSyntax operand, string op, string? mask, strin { if (operand is not IdentifierNameSyntax id || !_locals.TryGetValue(id.Identifier.Text, out var kind)) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"'{op}' on '{Trunc(operand)}' (only lane locals support ++/-- inside the loop body)", loc); } @@ -717,9 +720,9 @@ private void EmitIncDec(ExpressionSyntax operand, string op, string? mask, strin _ = _affineOffset.Remove(name); // ++/-- keeps affine-ness but conservatively drop it string one = kind switch { - SpmdGenerator.Kind.F => "new VFloat(1f)", - SpmdGenerator.Kind.D => $"new {VType(SpmdGenerator.Kind.D)}(1d)", - SpmdGenerator.Kind.L => $"new {VType(SpmdGenerator.Kind.L)}(1)", + Kind.F => "new VFloat(1f)", + Kind.D => $"new {VType(Kind.D)}(1d)", + Kind.L => $"new {VType(Kind.L)}(1)", _ => "new VInt(1)", }; string rhs = $"({name} {(op == "++" ? "+" : "-")} {one})"; @@ -728,13 +731,13 @@ private void EmitIncDec(ExpressionSyntax operand, string op, string? mask, strin : $"{indent}{name} = {SelectOf(kind)}({mask}, {rhs}, {name});"); } - private string CombineCompound(string op, string currentName, AssignmentExpressionSyntax asg, SpmdGenerator.Kind kind) + private string CombineCompound(string op, string currentName, AssignmentExpressionSyntax asg, Kind kind) => CombineCompoundOnExpr(op, currentName, asg, kind); - private string CombineCompoundOnExpr(string op, string current, AssignmentExpressionSyntax asg, SpmdGenerator.Kind kind) + private string CombineCompoundOnExpr(string op, string current, AssignmentExpressionSyntax asg, Kind kind) { string rhs = Coerce(Vec(asg.Right), kind, asg.Right); - bool isInt = kind is SpmdGenerator.Kind.I or SpmdGenerator.Kind.L; + bool isInt = kind is Kind.I or Kind.L; return op switch { "=" => rhs, @@ -746,7 +749,7 @@ private string CombineCompoundOnExpr(string op, string current, AssignmentExpres "&=" when isInt => $"({current} & {rhs})", "|=" when isInt => $"({current} | {rhs})", "^=" when isInt => $"({current} ^ {rhs})", - _ => throw new SpmdGenerator.UnsupportedConstructException( + _ => throw new UnsupportedConstructException( $"assignment operator '{op}' on {kind} lanes", asg.GetLocation()), }; } @@ -889,7 +892,7 @@ private void EmitFor(ForStatementSyntax fors, string? mask, string indent, Strin string varType = fors.Declaration.Type.ToString(); if (varType != "int") { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"for-loop variable of type '{varType}' (only 'int' uniform loop variables supported)", fors.Declaration.GetLocation()); } @@ -898,7 +901,7 @@ private void EmitFor(ForStatementSyntax fors, string? mask, string indent, Strin { _ = _uniformPreLocals.Add(v.Identifier.Text); if (!_preLocalKinds.ContainsKey(v.Identifier.Text)) - _preLocalKinds[v.Identifier.Text] = SpmdGenerator.Kind.I; + _preLocalKinds[v.Identifier.Text] = Kind.I; } } @@ -1013,7 +1016,7 @@ private void EmitBreak(string? mask, string indent, StringBuilder sb, Location l { if (_loopStack.Count == 0) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( "'break' outside of a loop", loc); } @@ -1034,7 +1037,7 @@ private void EmitContinue(string? mask, string indent, StringBuilder sb, Locatio { if (_loopStack.Count == 0) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( "'continue' outside of a loop", loc); } @@ -1121,7 +1124,7 @@ private bool HasVaryingBreakContinue(ForStatementSyntax fors) return false; } - private (string Code, SpmdGenerator.Kind Kind) Vec(ExpressionSyntax e) => e switch + private (string Code, Kind Kind) Vec(ExpressionSyntax e) => e switch { ParenthesizedExpressionSyntax p => WrapParen(Vec(p.Expression)), @@ -1139,31 +1142,31 @@ IdentifierNameSyntax id when _locals.TryGetValue(id.Identifier.Text, out var k) => (id.Identifier.Text, k), IdentifierNameSyntax id when _uniformPreLocals.Contains(id.Identifier.Text) - => Broadcast(id.Identifier.Text, _l ? SpmdGenerator.Kind.L : _d ? SpmdGenerator.Kind.D : _preLocalKinds[id.Identifier.Text]), + => Broadcast(id.Identifier.Text, _l ? Kind.L : _d ? Kind.D : _preLocalKinds[id.Identifier.Text]), IdentifierNameSyntax id when _params.TryGetValue(id.Identifier.Text, out var p) && - p.PKind is SpmdGenerator.ParamKind.UniformFloat - or SpmdGenerator.ParamKind.UniformInt - or SpmdGenerator.ParamKind.UniformDouble - or SpmdGenerator.ParamKind.UniformLong + p.PKind is ParamKind.UniformFloat + or ParamKind.UniformInt + or ParamKind.UniformDouble + or ParamKind.UniformLong => Broadcast(id.Identifier.Text, _l - ? SpmdGenerator.Kind.L + ? Kind.L : _d - ? SpmdGenerator.Kind.D + ? Kind.D : p.PKind switch { - SpmdGenerator.ParamKind.UniformInt => SpmdGenerator.Kind.I, - SpmdGenerator.ParamKind.UniformDouble => SpmdGenerator.Kind.D, - SpmdGenerator.ParamKind.UniformLong => SpmdGenerator.Kind.L, - _ => SpmdGenerator.Kind.F, + ParamKind.UniformInt => Kind.I, + ParamKind.UniformDouble => Kind.D, + ParamKind.UniformLong => Kind.L, + _ => Kind.F, }), IdentifierNameSyntax id when id.Identifier.Text == _loopVar => _l - ? ("(VLong.ProgramIndex + __i)", SpmdGenerator.Kind.L) + ? ("(VLong.ProgramIndex + __i)", Kind.L) : _d - ? ("(VDouble.ProgramIndex + (double)__i)", SpmdGenerator.Kind.D) - : ("(VInt.ProgramIndex + __i)", SpmdGenerator.Kind.I), + ? ("(VDouble.ProgramIndex + (double)__i)", Kind.D) + : ("(VInt.ProgramIndex + __i)", Kind.I), // Struct array-member element: 's.field[k]' (constant k) → the backing gang 's.field_k'. ElementAccessExpressionSyntax sae when TryStructArrayElement(sae, out string? sag, out var sak) => (sag, sak), @@ -1176,7 +1179,7 @@ MemberAccessExpressionSyntax cma when TryConstMember(cma, out var cc) => cc, // Uniform '.Length' on a buffer/array param → broadcast int. MemberAccessExpressionSyntax lma when lma.Name.Identifier.Text == "Length" && IsUniformExpression(lma) - => _d ? ($"new VDouble({lma})", SpmdGenerator.Kind.D) : ($"new VInt({lma})", SpmdGenerator.Kind.I), + => _d ? ($"new VDouble({lma})", Kind.D) : ($"new VInt({lma})", Kind.I), // 2-D array access a[i, j] on a float[,]/int[,]/double[,] param. ElementAccessExpressionSyntax ea2 when ea2.ArgumentList.Arguments.Count == 2 && @@ -1200,24 +1203,24 @@ eaGather.Expression is IdentifierNameSyntax gatherId && InvocationExpressionSyntax call => VecCall(call), - _ => throw new SpmdGenerator.UnsupportedConstructException($"expression '{Trunc(e)}' ({e.Kind()})", e.GetLocation()), + _ => throw new UnsupportedConstructException($"expression '{Trunc(e)}' ({e.Kind()})", e.GetLocation()), }; - private static (string, SpmdGenerator.Kind) WrapParen((string Code, SpmdGenerator.Kind Kind) inner) + private static (string, Kind) WrapParen((string Code, Kind Kind) inner) => ($"({inner.Code})", inner.Kind); - private static (string, SpmdGenerator.Kind) Neg((string Code, SpmdGenerator.Kind Kind) inner) + private static (string, Kind) Neg((string Code, Kind Kind) inner) => ($"(-{inner.Code})", inner.Kind); - private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.Kind Kind) inner, SyntaxNode at) - => inner.Kind == SpmdGenerator.Kind.I - ? ($"(~{inner.Code})", SpmdGenerator.Kind.I) - : throw new SpmdGenerator.UnsupportedConstructException("'~' on non-int lanes", at.GetLocation()); + private static (string, Kind) BitNot((string Code, Kind Kind) inner, SyntaxNode at) + => inner.Kind == Kind.I + ? ($"(~{inner.Code})", Kind.I) + : throw new UnsupportedConstructException("'~' on non-int lanes", at.GetLocation()); - private (string, SpmdGenerator.Kind) Broadcast(string name, SpmdGenerator.Kind k) + private (string, Kind) Broadcast(string name, Kind k) => ($"new {VType(k)}({name})", k); - private (string, SpmdGenerator.Kind) LitExpr(LiteralExpressionSyntax lit) + private (string, Kind) LitExpr(LiteralExpressionSyntax lit) { string text = lit.Token.Text; @@ -1226,11 +1229,11 @@ private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.K { if (text.EndsWith("f") || text.EndsWith("F") || text.EndsWith("d") || text.EndsWith("D") || text.Contains('.')) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"floating literal '{text}' in a long kernel", lit.GetLocation()); } - return ($"new VLong({text.TrimEnd('L', 'l', 'u', 'U')})", SpmdGenerator.Kind.L); + return ($"new VLong({text.TrimEnd('L', 'l', 'u', 'U')})", Kind.L); } // Hex/binary literals are always int lanes ("0xFF" ends in 'F' but is not a float). @@ -1239,25 +1242,25 @@ private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.K if (isHexOrBinary) { return _d - ? ($"new VDouble({text})", SpmdGenerator.Kind.D) - : ($"new VInt({text})", SpmdGenerator.Kind.I); + ? ($"new VDouble({text})", Kind.D) + : ($"new VInt({text})", Kind.I); } if (_d) - return ($"new VDouble({text.TrimEnd('f', 'F', 'd', 'D')})", SpmdGenerator.Kind.D); + return ($"new VDouble({text.TrimEnd('f', 'F', 'd', 'D')})", Kind.D); // A 'd'-suffixed literal in a float/int kernel opts that expression into // double precision (VDouble2 pairs at full gang width). if (text.EndsWith("d") || text.EndsWith("D")) - return ($"new VDouble2({text.TrimEnd('d', 'D')})", SpmdGenerator.Kind.D); + return ($"new VDouble2({text.TrimEnd('d', 'D')})", Kind.D); // An 'L'-suffixed literal opts into 64-bit integers (VLong2 pairs at full gang width). if (text.EndsWith("L") || text.EndsWith("l")) - return ($"new VLong2({text.TrimEnd('u', 'U')})", SpmdGenerator.Kind.L); + return ($"new VLong2({text.TrimEnd('u', 'U')})", Kind.L); if (text.EndsWith("f") || text.EndsWith("F") || text.Contains('.')) - return ($"new VFloat({text.TrimEnd('f', 'F')}f)", SpmdGenerator.Kind.F); - return ($"new VInt({text})", SpmdGenerator.Kind.I); + return ($"new VFloat({text.TrimEnd('f', 'F')}f)", Kind.F); + return ($"new VInt({text})", Kind.I); } - private (string, SpmdGenerator.Kind) CastExpr(CastExpressionSyntax cast) + private (string, Kind) CastExpr(CastExpressionSyntax cast) { var inner = Vec(cast.Expression); string t = cast.Type.ToString(); @@ -1265,8 +1268,8 @@ private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.K { // Long kernel: (long)/(int) are identity on the 64-bit integer lane; no floats. if (t is "long" or "int") - return (inner.Code, SpmdGenerator.Kind.L); - throw new SpmdGenerator.UnsupportedConstructException( + return (inner.Code, Kind.L); + throw new UnsupportedConstructException( $"cast to '{t}' in a long kernel (only (long)/(int) casts supported)", cast.GetLocation()); } @@ -1276,53 +1279,53 @@ private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.K // and (int)/(long) truncate toward zero, the double stays the carrier type // (used mostly as gather/scatter indices, where VLong truncation reapplies). if (t == "double") - return (inner.Code, SpmdGenerator.Kind.D); + return (inner.Code, Kind.D); if (t is "int" or "long") { - Warn(SpmdGenerator.DoubleConvertPerf, cast, cast.ToString()); - return ($"VectorMath.Truncate({inner.Code})", SpmdGenerator.Kind.D); + Warn(Descriptors.DoubleConvertPerf, cast, cast.ToString()); + return ($"VectorMath.Truncate({inner.Code})", Kind.D); } - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"cast to '{t}' in a double kernel (only (double)/(int)/(long) casts supported)", cast.GetLocation()); } // Float/int kernels: cross-gang-width double conversions via VDouble2. - if (inner.Kind == SpmdGenerator.Kind.D) + if (inner.Kind == Kind.D) { if (t is "int" or "long") - Warn(SpmdGenerator.DoubleConvertPerf, cast, cast.ToString()); + Warn(Descriptors.DoubleConvertPerf, cast, cast.ToString()); return t switch { - "float" => ($"({inner.Code}).ToFloat()", SpmdGenerator.Kind.F), - "int" => ($"({inner.Code}).ToIntTruncate()", SpmdGenerator.Kind.I), - "double" => (inner.Code, SpmdGenerator.Kind.D), - _ => throw new SpmdGenerator.UnsupportedConstructException($"cast to '{t}'", cast.GetLocation()), + "float" => ($"({inner.Code}).ToFloat()", Kind.F), + "int" => ($"({inner.Code}).ToIntTruncate()", Kind.I), + "double" => (inner.Code, Kind.D), + _ => throw new UnsupportedConstructException($"cast to '{t}'", cast.GetLocation()), }; } // ...and cross-gang-width long conversions via VLong2. - if (inner.Kind == SpmdGenerator.Kind.L) + if (inner.Kind == Kind.L) { return t switch { - "long" => (inner.Code, SpmdGenerator.Kind.L), - "int" => ($"({inner.Code}).ToInt()", SpmdGenerator.Kind.I), - "float" => ($"({inner.Code}).ToFloat()", SpmdGenerator.Kind.F), - "double" => ($"({inner.Code}).ToDouble2()", SpmdGenerator.Kind.D), - _ => throw new SpmdGenerator.UnsupportedConstructException($"cast to '{t}'", cast.GetLocation()), + "long" => (inner.Code, Kind.L), + "int" => ($"({inner.Code}).ToInt()", Kind.I), + "float" => ($"({inner.Code}).ToFloat()", Kind.F), + "double" => ($"({inner.Code}).ToDouble2()", Kind.D), + _ => throw new UnsupportedConstructException($"cast to '{t}'", cast.GetLocation()), }; } return t switch { - "float" => (Coerce(inner, SpmdGenerator.Kind.F, cast), SpmdGenerator.Kind.F), - "int" => (Coerce(inner, SpmdGenerator.Kind.I, cast), SpmdGenerator.Kind.I), - "long" => (Coerce(inner, SpmdGenerator.Kind.L, cast), SpmdGenerator.Kind.L), - "double" => (Coerce(inner, SpmdGenerator.Kind.D, cast), SpmdGenerator.Kind.D), - _ => throw new SpmdGenerator.UnsupportedConstructException($"cast to '{t}'", cast.GetLocation()), + "float" => (Coerce(inner, Kind.F, cast), Kind.F), + "int" => (Coerce(inner, Kind.I, cast), Kind.I), + "long" => (Coerce(inner, Kind.L, cast), Kind.L), + "double" => (Coerce(inner, Kind.D, cast), Kind.D), + _ => throw new UnsupportedConstructException($"cast to '{t}'", cast.GetLocation()), }; } - private (string, SpmdGenerator.Kind) BinExpr(BinaryExpressionSyntax bin) + private (string, Kind) BinExpr(BinaryExpressionSyntax bin) { string op = bin.OperatorToken.Text; @@ -1332,9 +1335,9 @@ private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.K if (op is "<<" or ">>" or ">>>") { var (code, knd) = Vec(bin.Left); - if (knd is not (SpmdGenerator.Kind.I or SpmdGenerator.Kind.L)) + if (knd is not (Kind.I or Kind.L)) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"shift '{op}' on non-integer lanes", bin.GetLocation()); } @@ -1350,18 +1353,18 @@ private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.K }; } - if (shKind == SpmdGenerator.Kind.L) + if (shKind == Kind.L) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( "varying (per-lane) shift count on long lanes (only uniform shift counts on long)", bin.GetLocation()); } - string counts = Coerce(Vec(bin.Right), SpmdGenerator.Kind.I, bin.Right); + string counts = Coerce(Vec(bin.Right), Kind.I, bin.Right); return op switch { - "<<" => ($"VInt.ShiftLeftVariable({code}, {counts})", SpmdGenerator.Kind.I), - ">>" => ($"VInt.ShiftRightArithmeticVariable({code}, {counts})", SpmdGenerator.Kind.I), - _ => ($"VInt.ShiftRightLogicalVariable({code}, {counts})", SpmdGenerator.Kind.I), + "<<" => ($"VInt.ShiftLeftVariable({code}, {counts})", Kind.I), + ">>" => ($"VInt.ShiftRightArithmeticVariable({code}, {counts})", Kind.I), + _ => ($"VInt.ShiftRightLogicalVariable({code}, {counts})", Kind.I), }; } @@ -1372,12 +1375,12 @@ private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.K var kind = Promote(l.Kind, r.Kind); string lc = CoerceCode(l, kind, bin.Left); string rc = CoerceCode(r, kind, bin.Right); - bool isInt = kind is SpmdGenerator.Kind.I or SpmdGenerator.Kind.L; + bool isInt = kind is Kind.I or Kind.L; // Integer / and % have no SIMD instruction, a per-lane scalar loop. Flag it unless the // divisor is a compile-time constant (the JIT can then strength-reduce; still note it). if (isInt && op is "/" or "%") - Warn(SpmdGenerator.IntDividePerf, bin, bin.ToString()); + Warn(Descriptors.IntDividePerf, bin, bin.ToString()); // 'a * b + c' on float/double lanes fuses into one FMA (higher throughput, single rounding). if (op == "+") @@ -1396,10 +1399,10 @@ private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.K // inactive lanes so masked-off lanes can't raise DivideByZeroException. "/" => ($"{VType(kind)}.Divide({lc}, {GuardedDivisor(rc, kind)})", kind), "%" when isInt => ($"{VType(kind)}.Remainder({lc}, {GuardedDivisor(rc, kind)})", kind), - "%" => throw new SpmdGenerator.UnsupportedConstructException( + "%" => throw new UnsupportedConstructException( "'%' on float/double lanes (use int/long lanes, or x - Floor(x/y)*y)", bin.GetLocation()), "&" or "|" or "^" when isInt => ($"({lc} {op} {rc})", kind), - _ => throw new SpmdGenerator.UnsupportedConstructException($"operator '{op}'", bin.GetLocation()), + _ => throw new UnsupportedConstructException($"operator '{op}'", bin.GetLocation()), }; } @@ -1408,10 +1411,10 @@ private static (string, SpmdGenerator.Kind) BitNot((string Code, SpmdGenerator.K /// precision-changing coercion (both already the target kind), so MulAdd still tracks the /// scalar reference. Yields the two factor codes. /// - private bool TryFmaFactors(ExpressionSyntax e, SpmdGenerator.Kind target, out string a, out string b) + private bool TryFmaFactors(ExpressionSyntax e, Kind target, out string a, out string b) { a = b = ""; - if (target is not (SpmdGenerator.Kind.F or SpmdGenerator.Kind.D)) + if (target is not (Kind.F or Kind.D)) return false; if (Unparen(e) is not BinaryExpressionSyntax { OperatorToken.Text: "*" } mul) return false; @@ -1424,21 +1427,21 @@ private bool TryFmaFactors(ExpressionSyntax e, SpmdGenerator.Kind target, out st return true; } - private string GuardedDivisor(string divisor, SpmdGenerator.Kind kind) + private string GuardedDivisor(string divisor, Kind kind) => _exprMask == null ? divisor : $"{VType(kind)}.Select({_exprMask}, {divisor}, {VType(kind)}.One)"; - private static SpmdGenerator.Kind Promote(SpmdGenerator.Kind a, SpmdGenerator.Kind b) + private static Kind Promote(Kind a, Kind b) { - if (a == SpmdGenerator.Kind.D || b == SpmdGenerator.Kind.D) - return SpmdGenerator.Kind.D; - if (a == SpmdGenerator.Kind.F || b == SpmdGenerator.Kind.F) - return SpmdGenerator.Kind.F; - if (a == SpmdGenerator.Kind.L || b == SpmdGenerator.Kind.L) - return SpmdGenerator.Kind.L; - return SpmdGenerator.Kind.I; + if (a == Kind.D || b == Kind.D) + return Kind.D; + if (a == Kind.F || b == Kind.F) + return Kind.F; + if (a == Kind.L || b == Kind.L) + return Kind.L; + return Kind.I; } - private (string, SpmdGenerator.Kind) TernExpr(ConditionalExpressionSyntax tern) + private (string, Kind) TernExpr(ConditionalExpressionSyntax tern) { var t = Vec(tern.WhenTrue); var f = Vec(tern.WhenFalse); @@ -1449,7 +1452,7 @@ private static SpmdGenerator.Kind Promote(SpmdGenerator.Kind a, SpmdGenerator.Ki /// /// Lane kind of a scalar struct field. Array members must be indexed (f[k]). /// - private SpmdGenerator.Kind FieldKind(string structType, string field, SyntaxNode at) + private Kind FieldKind(string structType, string field, SyntaxNode at) { if (_structs.TryGetValue(structType, out var si)) { @@ -1459,7 +1462,7 @@ private SpmdGenerator.Kind FieldKind(string structType, string field, SyntaxNode { if (f.IsArray) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"array member '{field}' used without an index (write '{field}[k]' with a constant k)", at.GetLocation()); } @@ -1468,7 +1471,7 @@ private SpmdGenerator.Kind FieldKind(string structType, string field, SyntaxNode } } - throw new SpmdGenerator.UnsupportedConstructException($"field '{field}' on struct '{structType}'", at.GetLocation()); + throw new UnsupportedConstructException($"field '{field}' on struct '{structType}'", at.GetLocation()); } /// @@ -1476,7 +1479,7 @@ private SpmdGenerator.Kind FieldKind(string structType, string field, SyntaxNode /// s is a struct local, field is a [SpmdArray] member, and k is a /// compile-time integer literal. Yields the backing gang (s.field_k) and its lane kind. /// - private bool TryStructArrayElement(ElementAccessExpressionSyntax ea, out string gang, out SpmdGenerator.Kind kind) + private bool TryStructArrayElement(ElementAccessExpressionSyntax ea, out string gang, out Kind kind) { gang = ""; kind = default; @@ -1496,13 +1499,13 @@ private bool TryStructArrayElement(ElementAccessExpressionSyntax ea, out string var idxExpr = ea.ArgumentList.Arguments[0].Expression; if (idxExpr is not LiteralExpressionSyntax lit || !int.TryParse(lit.Token.ValueText, out int k)) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"array-member index '{Trunc(idxExpr)}' must be a compile-time integer literal (SoA-in-registers has no runtime-indexed lane)", ea.GetLocation()); } if (k < 0 || k >= f.ArrayLength) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"array-member index {k} out of range for '{f.Name}[{f.ArrayLength}]'", ea.GetLocation()); } @@ -1516,7 +1519,7 @@ private bool TryStructArrayElement(ElementAccessExpressionSyntax ea, out string /// /// Read of a struct field: v.field (struct local) or buf[i].field (struct buffer). /// - private bool TryStructFieldRead(MemberAccessExpressionSyntax ma, out (string, SpmdGenerator.Kind) result) + private bool TryStructFieldRead(MemberAccessExpressionSyntax ma, out (string, Kind) result) { result = default; string field = ma.Name.Identifier.Text; @@ -1552,7 +1555,7 @@ private bool TryStructFieldRead(MemberAccessExpressionSyntax ma, out (string, Sp // Whole-struct read 'buf[i]' → construct the varying struct from each field's gang load. var si = _structs[bs]; if (si.Fields.Count > 1) - Warn(SpmdGenerator.GatherPerf, ea, ea.ToString()); + Warn(Descriptors.GatherPerf, ea, ea.ToString()); string fields = string.Join(", ", si.Fields.Select(f => StructFieldGather(bn, bs, f.Name, bidx))); return ($"new {si.VName}({fields})", bs); } @@ -1568,7 +1571,7 @@ private bool TryStructFieldRead(MemberAccessExpressionSyntax ma, out (string, Sp return ($"{VName(structType)}.Select({Mask(tern.Condition)}, {code}, {falseCode})", structType); } default: - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"struct expression '{Trunc(e)}' (use a local, 'new S{{...}}', 'new S(...)', a helper call, or a ternary)", e.GetLocation()); } } @@ -1577,7 +1580,7 @@ private bool TryStructFieldRead(MemberAccessExpressionSyntax ma, out (string, Sp { string type = oc.Type.ToString(); if (!_structs.TryGetValue(type, out var si)) - throw new SpmdGenerator.UnsupportedConstructException($"'new {type}' (not a [SpmdStruct])", oc.GetLocation()); + throw new UnsupportedConstructException($"'new {type}' (not a [SpmdStruct])", oc.GetLocation()); string vt = si.VName; // Zero-argument construction 'new S()' → all-zero gangs (same as 'default'). @@ -1591,9 +1594,9 @@ private bool TryStructFieldRead(MemberAccessExpressionSyntax ma, out (string, Sp foreach (var ex in oc.Initializer.Expressions) { if (ex is not AssignmentExpressionSyntax a || a.Left is not IdentifierNameSyntax fn) - throw new SpmdGenerator.UnsupportedConstructException($"struct initializer '{Trunc(ex)}'", ex.GetLocation()); + throw new UnsupportedConstructException($"struct initializer '{Trunc(ex)}'", ex.GetLocation()); var field = si.Fields.FirstOrDefault(x => x.Name == fn.Identifier.Text) - ?? throw new SpmdGenerator.UnsupportedConstructException($"field '{fn.Identifier.Text}' on struct '{type}'", ex.GetLocation()); + ?? throw new UnsupportedConstructException($"field '{fn.Identifier.Text}' on struct '{type}'", ex.GetLocation()); if (field.IsArray) { // 'arr = new float[N]' (sized, no initializer) → zero-filled; leave the gangs at @@ -1616,14 +1619,14 @@ private bool TryStructFieldRead(MemberAccessExpressionSyntax ma, out (string, Sp // Positional constructor: new S(a, b, ...), args map to fields in declaration order. if (si.HasArrayField) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"positional 'new {type}(...)' isn't supported for a struct with array members (use an object initializer, e.g. 'new {type} {{ f = new float[]{{ ... }} }}')", oc.GetLocation()); } var ctorArgs = oc.ArgumentList?.Arguments ?? default; if (ctorArgs.Count != si.Fields.Count) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"'new {type}(...)' expects {si.Fields.Count} field args (or use 'new {type} {{ field = ... }}')", oc.GetLocation()); } @@ -1648,13 +1651,13 @@ private List ExtractArrayElements(ExpressionSyntax e, int n) }; if (xs is null) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"array-member initializer '{Trunc(e)}' (use 'new float[]{{ ... }}')", e.GetLocation()); } if (xs.Count != n) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"array-member initializer has {xs.Count} elements, expected {n}", e.GetLocation()); } @@ -1669,7 +1672,7 @@ private string EmitCallArgs(InvocationExpressionSyntax call, FunctionInfo fn) var args = call.ArgumentList.Arguments; if (args.Count != fn.Parameters.Count) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"call to '{fn.Name}' expects {fn.Parameters.Count} arguments", call.GetLocation()); } @@ -1721,7 +1724,7 @@ private string StructFieldGather(string bufName, string structType, string field if (n == 1) return $"{VType(k)}.Load({flat}, {baseIdx})"; string idxVec = $"(({IntT}.ProgramIndex + ({baseIdx})) * {n} + {fi})"; - return k == SpmdGenerator.Kind.F + return k == Kind.F ? $"Memory.Gather({flat}, {idxVec})" : $"Memory.Gather({flat}, {idxVec}, VMask.All, 0)"; } @@ -1735,7 +1738,7 @@ private void EmitStructFieldStore(string bufName, string structType, string fiel var k = FieldKind(structType, field, asg); if (op != "=" && _structs[structType].Fields.Count > 1) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"compound '{op}' on struct-buffer field '{field}' (only '=' supported for AoS scatter)", asg.GetLocation()); } @@ -1770,7 +1773,7 @@ private void EmitStructFieldStoreRaw(string bufName, string structType, string f _ = sb.AppendLine($"{indent}Memory.Scatter({flat}, {idxVec}, {valueCode}, {mask ?? MTAll});"); } - private (string Code, SpmdGenerator.Kind Kind) VecCall(InvocationExpressionSyntax call) + private (string Code, Kind Kind) VecCall(InvocationExpressionSyntax call) { string callee = call.Expression.ToString().Replace(" ", ""); @@ -1779,7 +1782,7 @@ private void EmitStructFieldStoreRaw(string bufName, string structType, string f { if (_structs.ContainsKey(fnInfo.ReturnType)) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"struct-returning call '{fnInfo.Name}' used where a scalar is expected", call.GetLocation()); } @@ -1810,12 +1813,12 @@ private void EmitStructFieldStoreRaw(string bufName, string structType, string f "Math.Cbrt" or "MathF.Cbrt" => "Cbrt", "MathF.FusedMultiplyAdd" or "Math.FusedMultiplyAdd" => "!FMA", _ => null, - } ?? throw new SpmdGenerator.UnsupportedConstructException($"call to '{callee}' (no VectorMath mapping)", call.GetLocation()); + } ?? throw new UnsupportedConstructException($"call to '{callee}' (no VectorMath mapping)", call.GetLocation()); // Long kernels only have integer math, Min/Max/Abs (no transcendentals on 64-bit ints). if (_l && fn is not ("Min" or "Max" or "Abs")) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"'{callee}' on long lanes (only Math.Min/Max/Abs are available for 64-bit integers)", call.GetLocation()); } @@ -1829,12 +1832,12 @@ private void EmitStructFieldStoreRaw(string bufName, string structType, string f // Min/Max/Abs over 64-bit integer lanes stay in long precision (VLong2), the same way // a double argument pins the call to double, VectorMath has matching VLong2 overloads. bool longIntCall = !_l && fn is "Min" or "Max" or "Abs" && - vecArgs.Any(a => a.Value.Kind == SpmdGenerator.Kind.L) && - vecArgs.All(a => a.Value.Kind is SpmdGenerator.Kind.L or SpmdGenerator.Kind.I); - var target = _d || vecArgs.Any(a => a.Value.Kind == SpmdGenerator.Kind.D) - ? SpmdGenerator.Kind.D + vecArgs.Any(a => a.Value.Kind == Kind.L) && + vecArgs.All(a => a.Value.Kind is Kind.L or Kind.I); + var target = _d || vecArgs.Any(a => a.Value.Kind == Kind.D) + ? Kind.D : longIntCall - ? SpmdGenerator.Kind.L + ? Kind.L : LaneFloatKind; string[] args = [.. vecArgs.Select(a => CoerceCode(a.Value, target, a.Node))]; string code = fn == "!FMA" @@ -1848,7 +1851,7 @@ private void EmitStructFieldStoreRaw(string bufName, string structType, string f ParenthesizedExpressionSyntax p => $"({Mask(p.Expression)})", PrefixUnaryExpressionSyntax { OperatorToken.Text: "!" } not => $"(!{Mask(not.Operand)})", BinaryExpressionSyntax bin => MaskBin(bin), - _ => throw new SpmdGenerator.UnsupportedConstructException($"condition '{Trunc(e)}'", e.GetLocation()), + _ => throw new UnsupportedConstructException($"condition '{Trunc(e)}'", e.GetLocation()), }; private string MaskBin(BinaryExpressionSyntax bin) @@ -1871,39 +1874,39 @@ private string MaskBin(BinaryExpressionSyntax bin) "<" or ">" or "<=" or ">=" => $"({lc} {op} {rc})", "==" => $"{vt}.Eq({lc}, {rc})", "!=" => $"{vt}.Neq({lc}, {rc})", - _ => throw new SpmdGenerator.UnsupportedConstructException($"condition operator '{op}'", bin.GetLocation()), + _ => throw new UnsupportedConstructException($"condition operator '{op}'", bin.GetLocation()), }; } - private string Coerce((string Code, SpmdGenerator.Kind Kind) v, SpmdGenerator.Kind target, SyntaxNode at) + private string Coerce((string Code, Kind Kind) v, Kind target, SyntaxNode at) => CoerceCode(v, target, at); - private string CoerceCode((string Code, SpmdGenerator.Kind Kind) v, SpmdGenerator.Kind target, SyntaxNode at) + private string CoerceCode((string Code, Kind Kind) v, Kind target, SyntaxNode at) { if (v.Kind == target) return v.Code; - if (v.Kind == SpmdGenerator.Kind.I && target == SpmdGenerator.Kind.F) + if (v.Kind == Kind.I && target == Kind.F) return $"({v.Code}).ToFloat()"; // float -> int requires an explicit cast in the source, which arrives via CastExpr. - if (v.Kind == SpmdGenerator.Kind.F && target == SpmdGenerator.Kind.I) + if (v.Kind == Kind.F && target == Kind.I) return $"VInt.FromFloatTruncate({v.Code})"; // Implicit float/int -> double widening in float/int kernels (mirrors C#): // values become VDouble2 pairs at full gang width. - if (!_d && target == SpmdGenerator.Kind.D && v.Kind == SpmdGenerator.Kind.F) + if (!_d && target == Kind.D && v.Kind == Kind.F) return $"VDouble2.FromFloat({v.Code})"; - if (!_d && target == SpmdGenerator.Kind.D && v.Kind == SpmdGenerator.Kind.I) + if (!_d && target == Kind.D && v.Kind == Kind.I) return $"VDouble2.FromInt({v.Code})"; // int -> long widening in float/int kernels (mirrors C#): values become VLong2 pairs. // (float -> long only via an explicit cast; both flow through here.) - if (!_l && target == SpmdGenerator.Kind.L && v.Kind == SpmdGenerator.Kind.I) + if (!_l && target == Kind.L && v.Kind == Kind.I) return $"VLong2.FromInt({v.Code})"; - if (!_l && target == SpmdGenerator.Kind.L && v.Kind == SpmdGenerator.Kind.F) + if (!_l && target == Kind.L && v.Kind == Kind.F) return $"VLong2.FromFloatTruncate({v.Code})"; // long -> double widening in float/int kernels (mirrors C#). - if (!_d && !_l && target == SpmdGenerator.Kind.D && v.Kind == SpmdGenerator.Kind.L) + if (!_d && !_l && target == Kind.D && v.Kind == Kind.L) return $"({v.Code}).ToDouble2()"; // double -> float/int and long -> float/int narrowing requires an explicit cast (CastExpr). - throw new SpmdGenerator.UnsupportedConstructException($"lane conversion at '{Trunc(at)}'", at.GetLocation()); + throw new UnsupportedConstructException($"lane conversion at '{Trunc(at)}'", at.GetLocation()); } /// @@ -1999,46 +2002,46 @@ private static bool IsDefaultExpr(ExpressionSyntax e) /// Lower buf[non-loop-index] to Memory.Gather(buf, indices). /// The index expression is evaluated as a VInt (per-lane indices), then gathered. /// - private (string Code, SpmdGenerator.Kind Kind) EmitGather( - ElementAccessExpressionSyntax ea, string bufName, SpmdGenerator.ParamInfo p) + private (string Code, Kind Kind) EmitGather( + ElementAccessExpressionSyntax ea, string bufName, ParamInfo p) { if (ea.ArgumentList.Arguments.Count != 1) { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"multi-dimensional gather from '{bufName}' (only single-index a[expr] supported)", ea.GetLocation()); } - Warn(SpmdGenerator.GatherPerf, ea, ea.ToString()); + Warn(Descriptors.GatherPerf, ea, ea.ToString()); var idxExpr = ea.ArgumentList.Arguments[0].Expression; // Long kernels index with VLong directly (the lane expression is already 64-bit). if (_l) { - string idxL = Coerce(Vec(idxExpr), SpmdGenerator.Kind.L, idxExpr); - return ($"Memory.Gather({bufName}, {idxL})", SpmdGenerator.Kind.L); + string idxL = Coerce(Vec(idxExpr), Kind.L, idxExpr); + return ($"Memory.Gather({bufName}, {idxL})", Kind.L); } // Double kernels index with VLong (truncated from the double lane expression). if (_d) { - string idxD = Coerce(Vec(idxExpr), SpmdGenerator.Kind.D, idxExpr); - return ($"Memory.Gather({bufName}, VLong.FromDoubleTruncate({idxD}))", SpmdGenerator.Kind.D); + string idxD = Coerce(Vec(idxExpr), Kind.D, idxExpr); + return ($"Memory.Gather({bufName}, VLong.FromDoubleTruncate({idxD}))", Kind.D); } - string idx = Coerce(Vec(idxExpr), SpmdGenerator.Kind.I, idxExpr); + string idx = Coerce(Vec(idxExpr), Kind.I, idxExpr); // Memory.Gather(ReadOnlySpan, VInt) → VFloat // Memory.Gather(ReadOnlySpan, VInt, VMask, int) → VInt - if (p.ElemKind == SpmdGenerator.Kind.F) - return ($"Memory.Gather({bufName}, {idx})", SpmdGenerator.Kind.F); + if (p.ElemKind == Kind.F) + return ($"Memory.Gather({bufName}, {idx})", Kind.F); else - return ($"Memory.Gather({bufName}, {idx}, VMask.All, 0)", SpmdGenerator.Kind.I); + return ($"Memory.Gather({bufName}, {idx}, VMask.All, 0)", Kind.I); } /// /// Recognize a known scalar constant member (MathF.PI, float.MaxValue, int.MinValue, ...). /// - private bool TryConstMember(MemberAccessExpressionSyntax ma, out (string, SpmdGenerator.Kind) result) + private bool TryConstMember(MemberAccessExpressionSyntax ma, out (string, Kind) result) { result = default; string member = ma.Name.Identifier.Text; @@ -2049,18 +2052,18 @@ private bool TryConstMember(MemberAccessExpressionSyntax ma, out (string, SpmdGe } string full = ma.ToString().Replace(" ", ""); - SpmdGenerator.Kind k; + Kind k; if (full.StartsWith("MathF.") || full.StartsWith("float.")) - k = SpmdGenerator.Kind.F; + k = Kind.F; else if (full.StartsWith("Math.") || full.StartsWith("double.")) - k = SpmdGenerator.Kind.D; + k = Kind.D; else if (full.StartsWith("int.")) - k = SpmdGenerator.Kind.I; + k = Kind.I; else return false; if (_d) - k = SpmdGenerator.Kind.D; // double kernel: everything is a VDouble lane + k = Kind.D; // double kernel: everything is a VDouble lane result = ($"new {VType(k)}({ma})", k); return true; } @@ -2068,28 +2071,28 @@ private bool TryConstMember(MemberAccessExpressionSyntax ma, out (string, SpmdGe /// /// Lower a 2-D array read a[i, j] to a contiguous load (row-major) or a gather. /// - private (string Code, SpmdGenerator.Kind Kind) Emit2DLoad(ElementAccessExpressionSyntax ea, SpmdGenerator.ParamInfo p) + private (string Code, Kind Kind) Emit2DLoad(ElementAccessExpressionSyntax ea, ParamInfo p) { var idx0 = ea.ArgumentList.Arguments[0].Expression; var idx1 = ea.ArgumentList.Arguments[1].Expression; if (TryGet2DContiguous(idx0, idx1, p.ColsName, out string? flatBase)) return ($"{VType(p.ElemKind)}.Load({p.FlatName}, {flatBase})", p.ElemKind); - Warn(SpmdGenerator.GatherPerf, ea, ea.ToString()); + Warn(Descriptors.GatherPerf, ea, ea.ToString()); string flatIdx = Build2DFlatIndex(idx0, idx1, p.ColsName); if (_l) - return ($"Memory.Gather({p.FlatName}, {flatIdx})", SpmdGenerator.Kind.L); + return ($"Memory.Gather({p.FlatName}, {flatIdx})", Kind.L); if (_d) - return ($"Memory.Gather({p.FlatName}, {flatIdx})", SpmdGenerator.Kind.D); - if (p.ElemKind == SpmdGenerator.Kind.F) - return ($"Memory.Gather({p.FlatName}, {flatIdx})", SpmdGenerator.Kind.F); - return ($"Memory.Gather({p.FlatName}, {flatIdx}, VMask.All, 0)", SpmdGenerator.Kind.I); + return ($"Memory.Gather({p.FlatName}, {flatIdx})", Kind.D); + if (p.ElemKind == Kind.F) + return ($"Memory.Gather({p.FlatName}, {flatIdx})", Kind.F); + return ($"Memory.Gather({p.FlatName}, {flatIdx}, VMask.All, 0)", Kind.I); } /// /// Lower a 2-D array store a[i, j] = ... to a contiguous store or a scatter. /// - private void Emit2DStore(ElementAccessExpressionSyntax ea, SpmdGenerator.ParamInfo p, + private void Emit2DStore(ElementAccessExpressionSyntax ea, ParamInfo p, AssignmentExpressionSyntax asg, string op, string? mask, string indent, StringBuilder sb) { var idx0 = ea.ArgumentList.Arguments[0].Expression; @@ -2109,11 +2112,11 @@ private void Emit2DStore(ElementAccessExpressionSyntax ea, SpmdGenerator.ParamIn if (op != "=") { - throw new SpmdGenerator.UnsupportedConstructException( + throw new UnsupportedConstructException( $"compound scatter '{op}' on 2-D array '{p.Name}' (only '=' supported for non-contiguous stores)", asg.GetLocation()); } - Warn(SpmdGenerator.ScatterPerf, ea, ea.ToString()); + Warn(Descriptors.ScatterPerf, ea, ea.ToString()); string flatIdx = Build2DFlatIndex(idx0, idx1, p.ColsName); string val = Coerce(Vec(asg.Right), ek, asg.Right); string maskArg = mask ?? MTAll; @@ -2165,7 +2168,7 @@ private string BuildIntIndex(ExpressionSyntax e) => Wide ? "(VLong.ProgramIndex + (long)__i)" : "(VInt.ProgramIndex + __i)", BinaryExpressionSyntax bin when bin.OperatorToken.Text is "+" or "-" or "*" => $"({BuildIntIndex(bin.Left)} {bin.OperatorToken.Text} {BuildIntIndex(bin.Right)})", - _ => throw new SpmdGenerator.UnsupportedConstructException( + _ => throw new UnsupportedConstructException( $"non-affine 2-D index '{Trunc(e)}' (indices must be affine in the loop variable and uniforms)", e.GetLocation()), }; } @@ -2175,35 +2178,4 @@ private static string Trunc(SyntaxNode n) string s = n.ToString(); return s.Length > 60 ? s.Substring(0, 57) + "..." : s; } - - /// - /// Tracks break/continue state for the innermost loop being emitted. - /// For uniform loops (plain C# for), break/continue map to C# keywords. - /// For varying loops (mask iteration), break/continue manipulate masks. - /// - private readonly struct LoopContext - { - public readonly string BreakMask; - public readonly string ContinueMask; - public readonly string LoopMask; - public readonly bool IsUniform; - - public LoopContext(string breakMask, string continueMask, string loopMask) - { - BreakMask = breakMask; - ContinueMask = continueMask; - LoopMask = loopMask; - IsUniform = false; - } - - private LoopContext(bool isUniform) - { - BreakMask = ""; - ContinueMask = ""; - LoopMask = ""; - IsUniform = isUniform; - } - - public static LoopContext Uniform() => new(true); - } } diff --git a/src/IspcSharp/SpmdArrayAttribute.cs b/src/IspcSharp/SpmdArrayAttribute.cs new file mode 100644 index 0000000..18e540f --- /dev/null +++ b/src/IspcSharp/SpmdArrayAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace IspcSharp; + +/// +/// Declares a float[]/int[]/double[]/long[] field of a [SpmdStruct] +/// as an ISPC-style fixed-size array member of the given length. The varying companion expands +/// it into that many independent gangs held in registers (Structure-of-Arrays), so element access +/// s.field[k] resolves to a single gang. The index k must be a compile-time integer +/// literal (registers have no runtime-indexed lane), and such a struct is a local / helper argument / +/// return only, not a buffer element. Example: [SpmdArray(4)] public float[] Weights;. +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] +public sealed class SpmdArrayAttribute(int length) : Attribute +{ + /// + /// Fixed element count of the array member (must be > 0). + /// + public int Length { get; } = length; +} diff --git a/src/IspcSharp/SpmdAttribute.cs b/src/IspcSharp/SpmdAttribute.cs new file mode 100644 index 0000000..746281e --- /dev/null +++ b/src/IspcSharp/SpmdAttribute.cs @@ -0,0 +1,29 @@ +using System; + +namespace IspcSharp; + +/// +/// Marks a method for SPMD vectorization by the IspcSharp.Generators source generator. +/// The method body must be a single foreach (var i in Spmd.Range(n)) { ... } loop +/// using the supported C# subset (see README). A vectorized companion method named +/// {Name}_Simd is generated in the same partial class. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public sealed class SpmdAttribute : Attribute +{ + /// + /// Also generate a {Name}_ParallelSimd variant that splits across cores. + /// + public bool GenerateParallel { get; set; } = true; + + /// + /// Emit non-temporal (streaming, movntps) stores for the contiguous buffer writes, plus a + /// store fence after the loop. This bypasses the cache, a win only for large, write-once output + /// that won't be re-read soon (it avoids read-for-ownership traffic), and a loss if the data is + /// reused. NT stores need an aligned destination; the runtime falls back to an ordinary store + /// when a buffer isn't aligned, so it is always safe but only speeds up aligned buffers (large + /// arrays, where streaming helps, are commonly page-aligned in practice). Applies to float/double + /// output buffers. Off by default. + /// + public bool Streaming { get; set; } = false; +} diff --git a/src/IspcSharp/SpmdFunctionAttribute.cs b/src/IspcSharp/SpmdFunctionAttribute.cs new file mode 100644 index 0000000..90d893a --- /dev/null +++ b/src/IspcSharp/SpmdFunctionAttribute.cs @@ -0,0 +1,16 @@ +using System; + +namespace IspcSharp; + +/// +/// Marks a reusable, side-effect-free helper for SPMD vectorization, ISPC's non-export +/// function. The generator emits a "varying" companion (each float parameter/return becomes +/// a VFloat lane, each blittable struct its varying form) that any [Spmd] kernel or +/// other [SpmdFunction] can call. The body uses the same supported subset as a kernel, but +/// takes scalar values instead of buffers and has no Spmd.Range loop, it operates on the +/// gang it is handed. Recursion and buffer parameters are not allowed. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public sealed class SpmdFunctionAttribute : Attribute +{ +} diff --git a/src/IspcSharp/SpmdStructAttribute.cs b/src/IspcSharp/SpmdStructAttribute.cs new file mode 100644 index 0000000..7c675ea --- /dev/null +++ b/src/IspcSharp/SpmdStructAttribute.cs @@ -0,0 +1,16 @@ +using System; + +namespace IspcSharp; + +/// +/// Marks a blittable struct as usable inside [Spmd]/[SpmdFunction] bodies. Fields are +/// float/int/double/long, or ISPC-style fixed-size array members declared +/// with . The generator emits a varying companion whose fields are +/// the per-lane gang types (VFloat/VInt/…, one gang per array element), so the struct +/// can be a kernel local, a helper argument/return, or a Structure-of-Arrays buffer element accessed +/// as buf[i].field (buffers require same-width scalar fields and no array members). +/// +[AttributeUsage(AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] +public sealed class SpmdStructAttribute : Attribute +{ +} diff --git a/src/IspcSharp/Verification.cs b/src/IspcSharp/Verification.cs index 32109fd..87a00bc 100644 --- a/src/IspcSharp/Verification.cs +++ b/src/IspcSharp/Verification.cs @@ -3,75 +3,6 @@ namespace IspcSharp; -/// -/// Marks a method for SPMD vectorization by the IspcSharp.Generators source generator. -/// The method body must be a single foreach (var i in Spmd.Range(n)) { ... } loop -/// using the supported C# subset (see README). A vectorized companion method named -/// {Name}_Simd is generated in the same partial class. -/// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class SpmdAttribute : Attribute -{ - /// - /// Also generate a {Name}_ParallelSimd variant that splits across cores. - /// - public bool GenerateParallel { get; set; } = true; - - /// - /// Emit non-temporal (streaming, movntps) stores for the contiguous buffer writes, plus a - /// store fence after the loop. This bypasses the cache, a win only for large, write-once output - /// that won't be re-read soon (it avoids read-for-ownership traffic), and a loss if the data is - /// reused. NT stores need an aligned destination; the runtime falls back to an ordinary store - /// when a buffer isn't aligned, so it is always safe but only speeds up aligned buffers (large - /// arrays, where streaming helps, are commonly page-aligned in practice). Applies to float/double - /// output buffers. Off by default. - /// - public bool Streaming { get; set; } = false; -} - -/// -/// Marks a reusable, side-effect-free helper for SPMD vectorization, ISPC's non-export -/// function. The generator emits a "varying" companion (each float parameter/return becomes -/// a VFloat lane, each blittable struct its varying form) that any [Spmd] kernel or -/// other [SpmdFunction] can call. The body uses the same supported subset as a kernel, but -/// takes scalar values instead of buffers and has no Spmd.Range loop, it operates on the -/// gang it is handed. Recursion and buffer parameters are not allowed. -/// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class SpmdFunctionAttribute : Attribute -{ -} - -/// -/// Marks a blittable struct as usable inside [Spmd]/[SpmdFunction] bodies. Fields are -/// float/int/double/long, or ISPC-style fixed-size array members declared -/// with . The generator emits a varying companion whose fields are -/// the per-lane gang types (VFloat/VInt/…, one gang per array element), so the struct -/// can be a kernel local, a helper argument/return, or a Structure-of-Arrays buffer element accessed -/// as buf[i].field (buffers require same-width scalar fields and no array members). -/// -[AttributeUsage(AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] -public sealed class SpmdStructAttribute : Attribute -{ -} - -/// -/// Declares a float[]/int[]/double[]/long[] field of a [SpmdStruct] -/// as an ISPC-style fixed-size array member of the given length. The varying companion expands -/// it into that many independent gangs held in registers (Structure-of-Arrays), so element access -/// s.field[k] resolves to a single gang. The index k must be a compile-time integer -/// literal (registers have no runtime-indexed lane), and such a struct is a local / helper argument / -/// return only, not a buffer element. Example: [SpmdArray(4)] public float[] Weights;. -/// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] -public sealed class SpmdArrayAttribute(int length) : Attribute -{ - /// - /// Fixed element count of the array member (must be > 0). - /// - public int Length { get; } = length; -} - /// /// One per-element mismatch found by . /// diff --git a/tests/IspcSharp.Tests/GeneratorCachingTests.cs b/tests/IspcSharp.Tests/GeneratorCachingTests.cs new file mode 100644 index 0000000..4c88c78 --- /dev/null +++ b/tests/IspcSharp.Tests/GeneratorCachingTests.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; +using System.Linq; +using IspcSharp.Generators; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace IspcSharp.Tests; + +public class GeneratorCachingTests +{ + private const string KernelSource = """ + using IspcSharp; + + namespace CacheDemo; + + [SpmdStruct] + public struct Vec2 + { + public float X; + public float Y; + } + + public static partial class Kernels + { + [SpmdFunction] + public static float Lerp(float a, float b, float t) => a + ((b - a) * t); + + [Spmd] + public static void Blend(float[] a, float[] b, float[] o, float t, int count) + { + foreach (int i in Spmd.Range(count)) + { + o[i] = Lerp(a[i], b[i], t); + } + } + } + """; + + private static readonly string[] ModelSteps = + ["SpmdStructs", "SpmdFunctions", "SpmdKernels", "SpmdStructTable", "SpmdFunctionTable"]; + + private static CSharpCompilation CreateCompilation(string source) + { + string tpa = (string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!; + var references = tpa.Split(Path.PathSeparator) + .Select(p => (MetadataReference)MetadataReference.CreateFromFile(p)) + .Append(MetadataReference.CreateFromFile(typeof(Spmd).Assembly.Location)) + .ToList(); + + return CSharpCompilation.Create( + "CacheTest", + [CSharpSyntaxTree.ParseText(source)], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + private static GeneratorDriver CreateDriver() + => CSharpGeneratorDriver.Create( + [new SpmdGenerator().AsSourceGenerator()], + driverOptions: new GeneratorDriverOptions( + IncrementalGeneratorOutputKind.None, + trackIncrementalGeneratorSteps: true)); + + [Fact] + public void Generator_ProducesSources_AndNoDiagnostics() + { + var driver = CreateDriver().RunGenerators(CreateCompilation(KernelSource)); + GeneratorRunResult result = driver.GetRunResult().Results[0]; + Assert.Empty(result.Diagnostics); + Assert.Contains(result.GeneratedSources, s => s.HintName == "Blend_Spmd.g.cs"); + Assert.Contains(result.GeneratedSources, s => s.HintName == "Lerp_SpmdFn.g.cs"); + Assert.Contains(result.GeneratedSources, s => s.HintName == "__SpmdStructs.g.cs"); + } + + [Fact] + public void Pipeline_IsFullyCached_WhenAnUnrelatedFileIsAdded() + { + var compilation = CreateCompilation(KernelSource); + var driver = CreateDriver().RunGenerators(compilation); + + var edited = compilation.AddSyntaxTrees( + CSharpSyntaxTree.ParseText("namespace CacheDemo { internal sealed class Unrelated { } }")); + driver = driver.RunGenerators(edited); + GeneratorRunResult result = driver.GetRunResult().Results[0]; + + foreach (string step in ModelSteps) + { + Assert.All( + result.TrackedSteps[step].SelectMany(s => s.Outputs), + o => Assert.True( + o.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged, + $"step '{step}' re-ran: {o.Reason}")); + } + + Assert.All( + result.TrackedOutputSteps.SelectMany(kv => kv.Value).SelectMany(s => s.Outputs), + o => Assert.True( + o.Reason == IncrementalStepRunReason.Cached, + $"an output step re-ran: {o.Reason}")); + } + + [Fact] + public void Pipeline_Regenerates_WhenTheKernelBodyChanges() + { + var compilation = CreateCompilation(KernelSource); + var driver = CreateDriver().RunGenerators(compilation); + + var edited = CreateCompilation( + KernelSource.Replace("o[i] = Lerp(a[i], b[i], t);", "o[i] = Lerp(a[i], b[i], t) * 2f;")); + driver = driver.RunGenerators(edited); + GeneratorRunResult result = driver.GetRunResult().Results[0]; + + var blend = result.GeneratedSources.Single(s => s.HintName == "Blend_Spmd.g.cs"); + Assert.Contains("2f", blend.SourceText.ToString()); + } +} diff --git a/tests/IspcSharp.Tests/IspcSharp.Tests.csproj b/tests/IspcSharp.Tests/IspcSharp.Tests.csproj index 7db7e00..c674a08 100644 --- a/tests/IspcSharp.Tests/IspcSharp.Tests.csproj +++ b/tests/IspcSharp.Tests/IspcSharp.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -10,13 +10,17 @@ - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - +