diff options
| author | Tim Foley <tfoleyNV@users.noreply.github.com> | 2019-02-19 11:46:05 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-02-19 11:46:05 -0800 |
| commit | 32135c5bdfb4d387f8227742a2d2fd555898aca8 (patch) | |
| tree | fe78cbff0927a41ec759ba859cc16dd1280f661b /source/slang/check.cpp | |
| parent | a3fd4e2bc40cfc77db953b14744c30e7a18e7c1d (diff) | |
First steps toward supporting interface-type parameters on shaders (#852)
* First steps toward supporting interface-type parameters on shaders
What's New
----------
From the perspective of a user, the main thing this change adds is the ability to declare top-level shader parameters (either at global scope, or in an entry-point parameter list) with interface types. For example, the following becomes possible:
```hlsl
// Define an interface to modify values
interface IModifier { float4 modify(float4 val); }
// Define some concrete implementations
struct Doubler : IModifier
{
float4 modify(float4 val) { return val + val; }
}
struct Squarer : IModifier { ... }
// Define a global shader parameter of interface type
IModifier gGlobalModifier;
// Define an entry point with an interface-type `uniform` parameter
void myShader(
unifrom IModifier entryPointModifier,
float4 inColor : COLOR,
out float4 outColor : SV_Target)
{
// Use the interface-type parameters to compute things
float4 color = inColor;
color = gGlobalModifier.modify(color);
color = entryPointModifier.modify(color);
outColor = color;
}
```
The user can specialize that shader by specifying the concrete types to use for global and entry-point parameters of interface types (e.g., plugging in `Doubler` for `gGlobalModifier` and `Squarer` for `entryPointModifier`).
The "plugging in" process is done in terms of a concept of both global and local "existential slots" which are a new `LayoutResourceKind` that represents the holes where concrete types need to be plugged in for existential/interface types.
In simple cases like the above, each interface-type parameter will yield a single existential slot in either the global or entry-point parameter layout. Users can query the start slot and number of slots for each shader parameter, just like they would for any other resource that a parameter can consume. Before generating specialized code, the user plugs in the name of the concrete type they would like to use for each slot using `spSetTypeNameForGlobalExistentialSlot` and/or `spSetTypeNameForEntryPointExistentialSlot`.
There are some major limitations to the implementation in this first change:
* Parameters must be of interface type (e.g., `IFoo`) and not an array (`IFoo[3]`), or buffer (`ConstantBuffer<IFoo>`) over an interface type. Similarly, `struct` types with interface-type fields still don't work.
* The work on interface-type function parameters still doesn't include support for `out` or `inout` parameters, nor for functions that return interface types (that isn't technically related to this change, but affects its usefullness).
* No work is being done to correctly lay out shader parameters once the concrete types for existential slots are known, so that this change really only works when the concrete type that gets plugged in is empty.
These limitations are severe enough that this feature isn't really usable as implemented in this change, and this merely represents a stepping stone toward a more complete implementation.
Implementation
--------------
The API side of thing largely mirrors what was already done to support passing strings for the type names to use for global/entry-point generic arguments, so there should be no major surprises there.
The logic in `check.cpp` computes the list of existential slots when creating unspecialized `Program`s and `EntryPoint`s (this is logically the "front end" of the compiler), and then checks the supplied argument types against what is expected in each slot when creating specialized `Program`s and `EntryPoint`s. This again mirrors how generic arguments are handled.
Type layout was extended to compute the number of existential slots that a type consumes, and will thus automatically assign ranges of slots to top-level and entry-point shader parameters in the same way it already allocates `register`s and `binding`s. The big missing feature is the ability to specialize a layout to account for the concrete types plugged into the existential-type slots.
IR generation for specialized programs and entry points was slightly extended so that it attaches information about the concrete types plugged into the existential slots, and the witness tables that show how they conform to the interface for that slot. The linking step needed some small tweaks to make sure that information gets copied over to the target-specific program when we start code generation.
The meat of the IR-level work is in `ir-bind-existentials.cpp`, which takes the information that was placed in the IR module by the generation/linking steps and uses it to rewrite shader parameters. For example, if there is a shader parameter `p` of type `IModifier`, and the corresponding existential slot has the type `Doubler` in it, we will rewrite the parameter to have type `Doubler`, and rewrite any uses of `p` to instead use `makeExistential(p, /*witness that Doubler conforms to IModifier*/)`.
Once the replacement is done on the parameters, the existing work for specializing existential-based code when the input type(s) are known kicks in and does the rest.
Testing
-------
A single compute test is added to validate that this feature works. It is narrowly tailored to not require any of the features not supported by the initial implementation (e.g., all of the concrete types used have no members).
The test case *does* include use of an associated type through one of these existential-type parameters, which has exposed a subtle bug in how "opening" of existential values is implemented in the front-end. Rather than fix the underlying problem, I cleaned up the code in the front-end to special case when the existential value being opened is a variable bound with `let`, to directly use a reference to that variable rather than introduce a temporary. Similarly, in the IR generation step, I added an optimization to make variables declared with `let` skip introducing an IR-level variable and just use the SSA value of their initializer directly instead.
* fixup: missing files
* fixup: incorrect type for unreachable return
* fixup: actually comment ir-bind-existentials.cpp
Diffstat (limited to 'source/slang/check.cpp')
| -rw-r--r-- | source/slang/check.cpp | 395 |
1 files changed, 326 insertions, 69 deletions
diff --git a/source/slang/check.cpp b/source/slang/check.cpp index 483db60bb..a9f84c5c3 100644 --- a/source/slang/check.cpp +++ b/source/slang/check.cpp @@ -557,6 +557,117 @@ namespace Slang return isDeclUsableAsStaticMember(decl); } + /// Move `expr` into a temporary variable and execute `func` on that variable. + /// + /// Returns an expression that wraps both the creation and initialization of + /// the temporary, and the computation created by `func`. + /// + template<typename F> + RefPtr<Expr> moveTemp(RefPtr<Expr> const& expr, F const& func) + { + RefPtr<VarDecl> varDecl = new VarDecl(); + varDecl->ParentDecl = nullptr; // TODO: need to fill this in somehow! + varDecl->checkState = DeclCheckState::Checked; + varDecl->nameAndLoc.loc = expr->loc; + varDecl->initExpr = expr; + varDecl->type.type = expr->type.type; + + auto varDeclRef = makeDeclRef(varDecl.Ptr()); + + RefPtr<LetExpr> letExpr = new LetExpr(); + letExpr->decl = varDecl; + + auto body = func(varDeclRef); + + letExpr->body = body; + letExpr->type = body->type; + + return letExpr; + } + + /// Execute `func` on a variable with the value of `expr`. + /// + /// If `expr` is just a reference to an immutable (e.g., `let`) variable + /// then this might use the existing variable. Otherwise it will create + /// a new variable to hold `expr`, using `moveTemp()`. + /// + template<typename F> + RefPtr<Expr> maybeMoveTemp(RefPtr<Expr> const& expr, F const& func) + { + if(auto varExpr = as<VarExpr>(expr)) + { + auto declRef = varExpr->declRef; + if(auto varDeclRef = declRef.as<LetDecl>()) + return func(varDeclRef); + } + + return moveTemp(expr, func); + } + + /// Return an expression that represents "opening" the existential `expr`. + /// + /// The type of `expr` must be an interface type, matching `interfaceDeclRef`. + /// + /// If we scope down the PL theory to just the case that Slang cares about, + /// a value of an existential type like `IMover` is a tuple of: + /// + /// * a concrete type `X` + /// * a witness `w` of the fact that `X` implements `IMover` + /// * a value `v` of type `X` + /// + /// "Opening" an existential value is the process of decomposing a single + /// value `e : IMover` into the pieces `X`, `w`, and `v`. + /// + /// Rather than return all those pieces individually, this operation + /// returns an expression that logically corresponds to `v`: an expression + /// of type `X`, where the type carries the knowledge that `X` implements `IMover`. + /// + RefPtr<Expr> openExistential( + RefPtr<Expr> expr, + DeclRef<InterfaceDecl> interfaceDeclRef) + { + // If `expr` refers to an immutable binding, + // then we can use it directly. If it refers + // to an arbitrary expression or a mutable + // binding, we will move its value into an + // immutable temporary so that we can use + // it directly. + // + auto interfaceDecl = interfaceDeclRef.getDecl(); + return maybeMoveTemp(expr, [&](DeclRef<VarDeclBase> varDeclRef) + { + RefPtr<ExtractExistentialType> openedType = new ExtractExistentialType(); + openedType->declRef = varDeclRef; + + RefPtr<ExtractExistentialSubtypeWitness> openedWitness = new ExtractExistentialSubtypeWitness(); + openedWitness->sub = openedType; + openedWitness->sup = expr->type.type; + openedWitness->declRef = varDeclRef; + + RefPtr<ThisTypeSubstitution> openedThisType = new ThisTypeSubstitution(); + openedThisType->outer = interfaceDeclRef.substitutions.substitutions; + openedThisType->interfaceDecl = interfaceDecl; + openedThisType->witness = openedWitness; + + DeclRef<InterfaceDecl> substDeclRef = DeclRef<InterfaceDecl>(interfaceDecl, openedThisType); + auto substDeclRefType = DeclRefType::Create(getSession(), substDeclRef); + + RefPtr<ExtractExistentialValueExpr> openedValue = new ExtractExistentialValueExpr(); + openedValue->declRef = varDeclRef; + openedValue->type = QualType(substDeclRefType); + + return openedValue; + }); + } + + /// If `expr` has existential type, then open it. + /// + /// Returns an expression that opens `expr` if it had existential type. + /// Otherwise returns `expr` itself. + /// + /// See `openExistential` for a discussion of what "opening" an + /// existential-type value means. + /// RefPtr<Expr> maybeOpenExistential(RefPtr<Expr> expr) { auto exprType = expr->type.type; @@ -584,45 +695,7 @@ namespace Slang { // Okay, here is the case that matters. // - - auto interfaceDecl = interfaceDeclRef.getDecl(); - - RefPtr<VarDecl> varDecl = new VarDecl(); - varDecl->ParentDecl = nullptr; // TODO: need to fill this in somehow! - varDecl->checkState = DeclCheckState::Checked; - varDecl->nameAndLoc.loc = expr->loc; - varDecl->initExpr = expr; - varDecl->type.type = expr->type.type; - - auto varDeclRef = makeDeclRef(varDecl.Ptr()); - - RefPtr<LetExpr> letExpr = new LetExpr(); - letExpr->decl = varDecl; - - RefPtr<ExtractExistentialType> openedType = new ExtractExistentialType(); - openedType->declRef = varDeclRef; - - RefPtr<ExtractExistentialSubtypeWitness> openedWitness = new ExtractExistentialSubtypeWitness(); - openedWitness->sub = openedType; - openedWitness->sup = expr->type.type; - openedWitness->declRef = varDeclRef; - - RefPtr<ThisTypeSubstitution> openedThisType = new ThisTypeSubstitution(); - openedThisType->outer = interfaceDeclRef.substitutions.substitutions; - openedThisType->interfaceDecl = interfaceDecl; - openedThisType->witness = openedWitness; - - DeclRef<InterfaceDecl> substDeclRef = DeclRef<InterfaceDecl>(interfaceDecl, openedThisType); - auto substDeclRefType = DeclRefType::Create(getSession(), substDeclRef); - - RefPtr<ExtractExistentialValueExpr> openedValue = new ExtractExistentialValueExpr(); - openedValue->declRef = varDeclRef; - openedValue->type = QualType(substDeclRefType); - - letExpr->body = openedValue; - letExpr->type = openedValue->type; - - return letExpr; + return openExistential(expr, interfaceDeclRef); } } } @@ -9232,13 +9305,78 @@ namespace Slang } } + /// Recursively walk `paramDeclRef` and add any required existential slots to `ioSlots`. + static void _collectExistentialParamsRec( + ExistentialSlots& ioSlots, + DeclRef<VarDeclBase> paramDeclRef) + { + auto type = GetType(paramDeclRef); + + // Whether or not something is an array does not affect + // the number of existential slots it introduces. + // + while( auto arrayType = as<ArrayExpressionType>(type) ) + { + type = arrayType->baseType; + } + + if( auto declRefType = as<DeclRefType>(type) ) + { + auto typeDeclRef = declRefType->declRef; + if( auto interfaceDeclRef = typeDeclRef.as<InterfaceDecl>() ) + { + // Each leaf parameter of interface type adds one slot. + // + ioSlots.types.Add(type); + } + else if( auto structDeclRef = typeDeclRef.as<StructDecl>() ) + { + // A structure type should recursively introduce + // existential slots for its fields. + // + for( auto fieldDeclRef : GetFields(structDeclRef) ) + { + if(fieldDeclRef.getDecl()->HasModifier<HLSLStaticModifier>()) + continue; + + _collectExistentialParamsRec(ioSlots, fieldDeclRef); + } + } + } + + // TODO: We eventually need to handle cases like constant + // buffers and parameter blocks that may have existential + // element types. + } + + /// Enumerate the existential-type parameters of an `EntryPoint`. + /// + /// Any parameters found will be added to the list of existential slots on `this`. + /// + void EntryPoint::_collectExistentialParams() + { + // Note: we defensively test whether there is a function decl-ref + // because this routine gets called from the constructor, and + // a "dummy" entry point will have a null pointer for the function. + // + if( auto funcDeclRef = getFuncDeclRef() ) + { + for( auto paramDeclRef : GetParameters(funcDeclRef) ) + { + _collectExistentialParamsRec(m_existentialSlots, paramDeclRef); + } + } + } + // Validate that an entry point function conforms to any additional // constraints based on the stage (and profile?) it specifies. void validateEntryPoint( - FuncDecl* entryPointFuncDecl, - Stage stage, + EntryPoint* entryPoint, DiagnosticSink* sink) { + auto entryPointFuncDecl = entryPoint->getFuncDecl(); + auto stage = entryPoint->getStage(); + // TODO: We currently do minimal checking here, but this is the // right place to perform the following validation checks: // @@ -9494,21 +9632,43 @@ namespace Slang } - // Now that we've *found* the entry point, it is time to validate - // that it actually meets the constraints for the chosen stage/profile. - // - validateEntryPoint( - entryPointFuncDecl, - entryPointProfile.GetStage(), - sink); - RefPtr<EntryPoint> entryPoint = EntryPoint::create( makeDeclRef(entryPointFuncDecl), entryPointProfile); + // Now that we've *found* the entry point, it is time to validate + // that it actually meets the constraints for the chosen stage/profile. + // + validateEntryPoint(entryPoint, sink); + return entryPoint; } + /// Enumerate the existential-type parameters of a `Program`. + /// + /// Any parameters found will be added to the list of existential slots on `this`. + /// + void Program::_collectExistentialParams() + { + // We need to inspect all of the global shader parameters + // referenced by the compile request, and for each we + // need to determine what existential types parameters it implies. + // + for( auto module : getModuleDependencies() ) + { + auto moduleDecl = module->getModuleDecl(); + for( auto globalVar : moduleDecl->getMembersOfType<VarDecl>() ) + { + if(!isGlobalShaderParameter(globalVar)) + continue; + + _collectExistentialParamsRec( + m_globalExistentialSlots, + makeDeclRef(globalVar.Ptr())); + } + } + } + /// Create a `Program` to represent the compiled code. /// /// The created program will comprise all of the translation @@ -9623,32 +9783,99 @@ namespace Slang Profile profile; profile.setStage(entryPointAttr->stage); - validateEntryPoint(funcDecl, entryPointAttr->stage, sink); - RefPtr<EntryPoint> entryPoint = EntryPoint::create( makeDeclRef(funcDecl), profile); + + validateEntryPoint(entryPoint, sink); + program->addEntryPoint(entryPoint); translationUnit->entryPoints.Add(entryPoint); } } } + program->_collectExistentialParams(); + return program; } - /// Create a specialization an existing entry point based on generic arguments. - DeclRef<FuncDecl> specializeEntryPoint( + static void _specializeExistentialSlots( Linkage* linkage, - FuncDecl* entryPointFuncDecl, + ExistentialSlots& ioSlots, + List<RefPtr<Expr>> const& args, + DiagnosticSink* sink) + { + UInt slotCount = ioSlots.types.Count(); + UInt argCount = args.Count(); + + if( slotCount != argCount ) + { + sink->diagnose(SourceLoc(), Diagnostics::mismatchExistentialSlotArgCount, slotCount, argCount); + return; + } + + SemanticsVisitor visitor(linkage, sink); + + for( UInt ii = 0; ii < slotCount; ++ii ) + { + auto slotType = ioSlots.types[ii]; + auto argExpr = args[ii]; + + auto argType = checkProperType(linkage, TypeExp(argExpr), sink); + if(!argType) + { + // TODO: Each slot should track a source location and/or a `VarDeclBase` + // that names the parameter that the slot corresponds to. + + sink->diagnose(SourceLoc(), Diagnostics::existentialSlotArgNotAType, ii); + return; + } + + + auto witness = visitor.tryGetSubtypeWitness(argType, slotType); + if (!witness) + { + // If no witness was found, then we will be unable to satisfy + // the conformances required. + sink->diagnose(SourceLoc(), Diagnostics::existentialSlotArgDoesNotConform, ii, slotType); + return; + } + + ExistentialSlots::Arg arg; + arg.type = argType; + arg.witness = witness; + ioSlots.args.Add(arg); + } + } + + void EntryPoint::_specializeExistentialSlots( + List<RefPtr<Expr>> const& args, + DiagnosticSink* sink) + { + Slang::_specializeExistentialSlots(getLinkage(), m_existentialSlots, args, sink); + } + + /// Create a specialization an existing entry point based on generic arguments. + RefPtr<EntryPoint> createSpecializedEntryPoint( + EntryPoint* unspecializedEntryPoint, List<RefPtr<Expr>> const& genericArgs, + List<RefPtr<Expr>> const& existentialArgs, DiagnosticSink* sink) { + auto linkage = unspecializedEntryPoint->getLinkage(); + + // TODO: Need to be careful in case entry point already has a decl-ref, + // pertaining to outer specializations (e.g., when entry point was + // nested in a generic type. + // + auto entryPointFuncDecl = unspecializedEntryPoint->getFuncDecl(); + SemanticsVisitor semantics( linkage, sink); - DeclRef<FuncDecl> entryPointFuncDeclRef = makeDeclRef(entryPointFuncDecl); + DeclRef<FuncDecl> entryPointFuncDeclRef = makeDeclRef(entryPointFuncDecl.Ptr()); if( auto genericDecl = as<GenericDecl>(entryPointFuncDecl->ParentDecl) ) { // We will construct a suitable `GenericAppExpr` to represent @@ -9702,7 +9929,7 @@ namespace Slang { // Any semantic error that occured should have been // reported already. - return DeclRef<FuncDecl>(); + return nullptr; } else { @@ -9710,11 +9937,18 @@ namespace Slang // function should always be a `DeclRefExpr` // SLANG_UNEXPECTED("reference to generic decl wasn't a `DeclRefExpr`"); - UNREACHABLE_RETURN(DeclRef<FuncDecl>()); + UNREACHABLE_RETURN(nullptr); } } - return entryPointFuncDeclRef; + RefPtr<EntryPoint> specializedEntryPoint = EntryPoint::create( + entryPointFuncDeclRef, + unspecializedEntryPoint->getProfile()); + + // Next we need to validate the existential arguments. + specializedEntryPoint->_specializeExistentialSlots(existentialArgs, sink); + + return specializedEntryPoint; } /// Parse an array of strings as generic arguments. @@ -9771,11 +10005,20 @@ namespace Slang } } + void Program::_specializeExistentialSlots( + List<RefPtr<Expr>> const& args, + DiagnosticSink* sink) + { + Slang::_specializeExistentialSlots(getLinkage(), m_globalExistentialSlots, args, sink); + } + + /// Specialize a program to global generic arguments RefPtr<Program> createSpecializedProgram( Linkage* linkage, Program* unspecializedProgram, List<RefPtr<Expr>> const& globalGenericArgs, + List<RefPtr<Expr>> const& globalExistentialArgs, DiagnosticSink* sink) { // The given `unspecializedProgram` should be one that @@ -9827,6 +10070,8 @@ namespace Slang // We have an appropriate number of arguments for the global generic parameters, // and now we need to check that the arguments conform to the declared constraints. // + SemanticsVisitor visitor(linkage, sink); + // Along the way, we will build up an appropriate set of substitutions to represent // the generic arguments and their conformances. // @@ -9905,7 +10150,6 @@ namespace Slang auto interfaceType = GetSup(DeclRef<GenericTypeConstraintDecl>(constraint, nullptr)); // Use our semantic-checking logic to search for a witness to the required conformance - SemanticsVisitor visitor(linkage, sink); auto witness = visitor.tryGetSubtypeWitness(globalGenericArg, interfaceType); if (!witness) { @@ -9937,6 +10181,10 @@ namespace Slang specializedProgram->setGlobalGenericSubsitution(globalGenericSubsts); + // Now deal with the existential arguments + specializedProgram->_collectExistentialParams(); + specializedProgram->_specializeExistentialSlots(globalExistentialArgs, sink); + return specializedProgram; } @@ -9949,12 +10197,11 @@ namespace Slang /// Returns a specialized entry point if everything worked as expected. /// Returns null and diagnoses errors if anything goes wrong. /// - RefPtr<EntryPoint> specializeEntryPoint( + RefPtr<EntryPoint> createSpecializedEntryPoint( EndToEndCompileRequest* endToEndReq, EntryPoint* unspecializedEntryPoint, EndToEndCompileRequest::EntryPointInfo const& entryPointInfo) { - auto linkage = endToEndReq->getLinkage(); auto sink = endToEndReq->getSink(); auto entryPointFuncDecl = unspecializedEntryPoint->getFuncDecl(); @@ -9967,19 +10214,21 @@ namespace Slang entryPointInfo.genericArgStrings, genericArgs); + List<RefPtr<Expr>> existentialArgs; + parseGenericArgStrings( + endToEndReq, + entryPointInfo.existentialArgStrings, + existentialArgs); + // Next we specialize the entry point function given the parsed // generic argument expressions. // - auto entryPointFuncDeclRef = specializeEntryPoint( - linkage, - entryPointFuncDecl, + auto entryPoint = createSpecializedEntryPoint( + unspecializedEntryPoint, genericArgs, + existentialArgs, sink); - RefPtr<EntryPoint> entryPoint = EntryPoint::create( - entryPointFuncDeclRef, - unspecializedEntryPoint->getProfile()); - return entryPoint; } @@ -10005,6 +10254,13 @@ namespace Slang endToEndReq->globalGenericArgStrings, globalGenericArgs); + // Also handle global existential type arguments. + List<RefPtr<Expr>> globalExistentialArgs; + parseGenericArgStrings( + endToEndReq, + endToEndReq->globalExistentialSlotArgStrings, + globalExistentialArgs); + // Now we create the initial specialized program by // applying the global generic arguments (if any) to the // unspecialized program. @@ -10013,6 +10269,7 @@ namespace Slang endToEndReq->getLinkage(), unspecializedProgram, globalGenericArgs, + globalExistentialArgs, endToEndReq->getSink()); // If anything went wrong with the global generic @@ -10045,7 +10302,7 @@ namespace Slang auto unspecializedEntryPoint = unspecializedProgram->getEntryPoint(ii); auto& entryPointInfo = endToEndReq->entryPoints[ii]; - auto specializedEntryPoint = specializeEntryPoint(endToEndReq, unspecializedEntryPoint, entryPointInfo); + auto specializedEntryPoint = createSpecializedEntryPoint(endToEndReq, unspecializedEntryPoint, entryPointInfo); specializedProgram->addEntryPoint(specializedEntryPoint); } |
