Skip to content

Commit af858c6

Browse files
committed
Validate generic struct capability requirements on entry-point params
Closes #9489 A user-defined generic struct with a [require(...)] capability, used as an entry-point parameter (e.g. `Foo<int>` where `Foo` is `[require(cpp)]`), was not validated against the compilation target, so it compiled silently for an incompatible target like spirv. The non-generic spelling `Foo` was already rejected with E36107. The general capability-inference walk records a parameter type's requirements only when the type's decl-ref is a DirectDeclRef; a generic specialization uses a GenericAppDeclRef and was skipped. Rather than broadening that walk for every function (which would force many core-module library functions taking generic struct params such as CoopVec<T,N> to redeclare capabilities), the missing generic-struct requirements are gathered at entry-point validation only. Builtin magic/intrinsic generic types (LineStream, OutputPatch, ...) are excluded since they have their own more specific entry-point diagnostics.
1 parent eb6a6ef commit af858c6

2 files changed

Lines changed: 419 additions & 6 deletions

File tree

source/slang/slang-check-shader.cpp

Lines changed: 186 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,110 @@ static bool _outputDeclHasSemantic(
13691369
}
13701370

13711371

1372+
// A user-defined generic struct found in an entry-point signature type, paired
1373+
// with the source location of its use.
1374+
struct GenericStructTypeUse
1375+
{
1376+
StructDecl* structDecl;
1377+
SourceLoc useLoc;
1378+
};
1379+
1380+
// Collect every user-defined generic struct (e.g. `Foo<int>`) reachable from an
1381+
// entry-point signature type `type`, recursing through wrapper/composite types so
1382+
// that `Foo<int>`, `Foo<int>[N]`, `Optional<Foo<int>>`, and
1383+
// `ConstantBuffer<Foo<int>>` are all found. Results are appended to `outUses` and
1384+
// `visited` guards against cycles in the `Val` graph.
1385+
//
1386+
// This is needed because the general capability-inference walk
1387+
// (`SemanticsDeclReferenceVisitor`) records a type's requirements only when its
1388+
// decl-ref is a `DirectDeclRef`; a generic specialization uses a
1389+
// `GenericAppDeclRef` and is skipped, so a `[require(...)]` on a generic struct
1390+
// used in an entry-point signature is otherwise dropped. The non-generic spelling
1391+
// `Foo` is already handled by that walk, so only the generic case is collected
1392+
// here (to avoid duplicate reporting).
1393+
//
1394+
// This deliberately lives in entry-point validation rather than in the general
1395+
// inference walk: inferring a generic struct type's requirements for *every*
1396+
// function that names such a type would require many core-module library
1397+
// functions (e.g. the cooperative vector/matrix/tensor `Load`/`Store` helpers,
1398+
// which take `CoopVec<T,N>` etc.) to redeclare those capabilities. Restricting
1399+
// the check to entry-point signatures matches the reported defect without
1400+
// changing library-function inference.
1401+
//
1402+
// Only the struct decl itself is filtered for `MagicTypeModifier`/
1403+
// `IntrinsicTypeModifier`: builtin generic types (e.g. `LineStream<T>`,
1404+
// `OutputPatch<T,N>`) already have dedicated, more specific entry-point
1405+
// diagnostics, so reporting a generic capability error for them would only
1406+
// duplicate those. Wrapper builtins are still recursed *through* so that a
1407+
// user-defined `Foo<int>` nested inside them is found.
1408+
static void collectGenericStructTypeUses(
1409+
ASTBuilder* astBuilder,
1410+
Val* type,
1411+
SourceLoc useLoc,
1412+
HashSet<Val*>& visited,
1413+
List<GenericStructTypeUse>& outUses,
1414+
UInt recursionDepth = 0)
1415+
{
1416+
if (!type || !visited.add(type))
1417+
return;
1418+
1419+
// Bound the recursion to avoid overflowing the stack on a legitimately deep
1420+
// acyclic chain (e.g. `Wrap<Wrap<...<Foo<int>>...>>`), where each level is a
1421+
// distinct hash-consed `Val` that the visited set does not collapse. This
1422+
// mirrors the `kMaxTypeNestingDepth` guard used by the other type walks in
1423+
// this file; a type nested past that limit is already diagnosed with
1424+
// "maximum type nesting level exceeded" by `validateVaryingType`, which runs
1425+
// earlier in `validateEntryPoint`, so we simply stop descending here.
1426+
if (recursionDepth >= kMaxTypeNestingDepth)
1427+
return;
1428+
1429+
if (auto declRefType = as<DeclRefType>(type))
1430+
{
1431+
auto structDeclRef = declRefType->getDeclRef().as<StructDecl>();
1432+
if (structDeclRef && as<GenericAppDeclRef>(declRefType->getDeclRefBase()) &&
1433+
!structDeclRef.getDecl()->findModifier<MagicTypeModifier>() &&
1434+
!structDeclRef.getDecl()->findModifier<IntrinsicTypeModifier>())
1435+
{
1436+
// Only contribute structs that actually carry a requirement; this keeps
1437+
// both the aggregation and the diagnostic loop free of null/empty sets.
1438+
auto* caps = structDeclRef.getDecl()->inferredCapabilityRequirements;
1439+
if (caps && !caps->isEmpty())
1440+
outUses.add({structDeclRef.getDecl(), useLoc});
1441+
}
1442+
// Recurse through the struct's fields *with substitutions applied*, so a
1443+
// wrapper like `struct Wrapper<T> { Foo<T> f; }` used as `Wrapper<int>`,
1444+
// or a non-generic `struct Wrapper { Foo<int> f; }`, still reaches
1445+
// `Foo<int>` (which the `Val`-operand walk below alone would miss, since
1446+
// the field type is not an operand of the wrapper type).
1447+
if (structDeclRef)
1448+
{
1449+
for (auto fieldDeclRef :
1450+
getFields(astBuilder, structDeclRef, MemberFilterStyle::Instance))
1451+
collectGenericStructTypeUses(
1452+
astBuilder,
1453+
getType(astBuilder, fieldDeclRef),
1454+
useLoc,
1455+
visited,
1456+
outUses,
1457+
recursionDepth + 1);
1458+
}
1459+
}
1460+
1461+
// Recurse into the type's `Val` operands (generic arguments, element types,
1462+
// etc.) so nested user generic structs inside wrappers/arrays are found.
1463+
for (Index i = 0; i < type->getOperandCount(); i++)
1464+
{
1465+
if (type->m_operands[i].kind == ValNodeOperandKind::ValNode)
1466+
collectGenericStructTypeUses(
1467+
astBuilder,
1468+
type->getOperand(i),
1469+
useLoc,
1470+
visited,
1471+
outUses,
1472+
recursionDepth + 1);
1473+
}
1474+
}
1475+
13721476
// Validate that an entry point function conforms to any additional
13731477
// constraints based on the stage (and profile?) it specifies.
13741478
void validateEntryPoint(EntryPoint* entryPoint, DiagnosticSink* sink)
@@ -1923,13 +2027,64 @@ void validateEntryPoint(EntryPoint* entryPoint, DiagnosticSink* sink)
19232027
}
19242028
}
19252029

2030+
// Augment the entry point's inferred requirements with the capability
2031+
// requirements of the *generic* struct types in its signature (parameters and
2032+
// return type). The general inference walk records a type's requirements only
2033+
// when its decl-ref is a `DirectDeclRef`, so a non-generic struct such as
2034+
// `Foo a` is already covered there, but a generic one such as `Foo<int> a`
2035+
// (whose decl-ref is a `GenericAppDeclRef`) is not. We gather the missing
2036+
// generic-struct requirements here so a `[require(...)]` on `Foo` is enforced
2037+
// for both spellings. `signatureStructUses` keeps each contributing struct and
2038+
// its use location so we can point the diagnostic at the exact use site (the
2039+
// non-generic case is reported by `diagnoseMissingCapabilityProvenance`).
2040+
CapabilitySet entryPointInferredCaps{entryPointFuncDecl->inferredCapabilityRequirements};
2041+
List<GenericStructTypeUse> signatureStructUses;
2042+
{
2043+
auto astBuilder = linkage->getASTBuilder();
2044+
// Use a fresh `visited` set per signature position. `Val` nodes are
2045+
// hash-consed, so the same specialization `Foo<int>` on two parameters is
2046+
// the identical `Val*`; a shared set would drop the second use site and the
2047+
// user would see only one "see using of 'Foo'" note. The per-position set
2048+
// still guards against cycles within a single type.
2049+
for (auto param : entryPointFuncDecl->getParameters())
2050+
{
2051+
// Prefer the written type-expression location (the `Foo<int>` use
2052+
// site); fall back to the parameter location if no type syntax was
2053+
// retained.
2054+
SourceLoc useLoc = (param->type.exp) ? param->type.exp->loc : param->loc;
2055+
HashSet<Val*> visited;
2056+
collectGenericStructTypeUses(
2057+
astBuilder,
2058+
param->getType(),
2059+
useLoc,
2060+
visited,
2061+
signatureStructUses);
2062+
}
2063+
// The return type has the same silent-compile bug as parameters: a
2064+
// `Foo<int> main()` whose `Foo` requires an unavailable capability must be
2065+
// diagnosed too.
2066+
SourceLoc returnLoc = (entryPointFuncDecl->returnType.exp)
2067+
? entryPointFuncDecl->returnType.exp->loc
2068+
: entryPointFuncDecl->loc;
2069+
HashSet<Val*> visited;
2070+
collectGenericStructTypeUses(
2071+
astBuilder,
2072+
entryPointFuncDecl->returnType.type,
2073+
returnLoc,
2074+
visited,
2075+
signatureStructUses);
2076+
}
2077+
// Every collected use carries a non-empty requirement (filtered in the
2078+
// collector), so this join is unconditional.
2079+
for (auto& use : signatureStructUses)
2080+
entryPointInferredCaps.nonDestructiveJoin(use.structDecl->inferredCapabilityRequirements);
2081+
19262082
for (auto target : linkage->targets)
19272083
{
19282084
auto targetCaps = target->getTargetCaps();
19292085
auto stageCapabilitySet = entryPoint->getProfile().getCapabilityName();
19302086
targetCaps.join(stageCapabilitySet);
1931-
if (targetCaps.isIncompatibleWith(
1932-
CapabilitySet{entryPointFuncDecl->inferredCapabilityRequirements}))
2087+
if (targetCaps.isIncompatibleWith(entryPointInferredCaps))
19332088
{
19342089
// Incompatable means we don't support a set of abstract atoms.
19352090
// Diagnose that we lack support for 'stage' and 'target' atoms with our provided
@@ -1952,6 +2107,33 @@ void validateEntryPoint(EntryPoint* entryPoint, DiagnosticSink* sink)
19522107
sink,
19532108
entryPointFuncDecl,
19542109
failedSet);
2110+
2111+
// The provenance walk above follows `capabilityRequirementProvenance`,
2112+
// which does not record generic struct signature types. Point at any
2113+
// such struct whose requirement is itself incompatible with the
2114+
// target, mirroring the notes emitted for non-generic structs.
2115+
for (auto& use : signatureStructUses)
2116+
{
2117+
if (!targetCaps.isIncompatibleWith(
2118+
CapabilitySet{use.structDecl->inferredCapabilityRequirements}))
2119+
continue;
2120+
maybeDiagnose(
2121+
sink,
2122+
linkage->m_optionSet,
2123+
DiagnosticCategory::Capability,
2124+
Diagnostics::SeeUsingOf{.decl = use.structDecl, .location = use.useLoc});
2125+
maybeDiagnose(
2126+
sink,
2127+
linkage->m_optionSet,
2128+
DiagnosticCategory::Capability,
2129+
Diagnostics::SeeDefinitionOf{.decl = use.structDecl});
2130+
if (auto requireAttr = use.structDecl->findModifier<RequireCapabilityAttribute>())
2131+
maybeDiagnose(
2132+
sink,
2133+
linkage->m_optionSet,
2134+
DiagnosticCategory::Capability,
2135+
Diagnostics::SeeDeclarationOfModifier{.modifier = requireAttr});
2136+
}
19552137
}
19562138
else
19572139
{
@@ -1989,13 +2171,11 @@ void validateEntryPoint(EntryPoint* entryPoint, DiagnosticSink* sink)
19892171

19902172
// Only attempt to error if a specific profile or capability is requested
19912173
if ((specificCapabilityRequested || specificProfileRequested) &&
1992-
targetCaps.atLeastOneSetImpliedInOther(
1993-
CapabilitySet{entryPointFuncDecl->inferredCapabilityRequirements}) ==
2174+
targetCaps.atLeastOneSetImpliedInOther(entryPointInferredCaps) ==
19942175
CapabilitySet::ImpliesReturnFlags::NotImplied)
19952176
{
19962177
CapabilitySet combinedSets = targetCaps;
1997-
combinedSets.join(
1998-
CapabilitySet{entryPointFuncDecl->inferredCapabilityRequirements});
2178+
combinedSets.join(entryPointInferredCaps);
19992179
CapabilityAtomSet addedAtoms{};
20002180
if (auto targetCapSet = targetCaps.getAtomSets())
20012181
{

0 commit comments

Comments
 (0)