Skip to content

Commit 4ab545b

Browse files
author
Tim Foley
authored
Initial work on support code generation for generics with constraints (shader-slang#233)
This change includes a lot of infrastructure work, but the main point is to allow code like the following: ``` // define an interface interface Helper { float help(); } // define a generic function that uses the interface float test<T : Helper>( T t ) { return t.help(); } // define a type that implements the interface struct A : Helper { float help() { return 1.0 } } // define an ordinary function that calls the // generic function with a concrete type: float doIt() { A a; return test<A>(a); } ``` Getting this to generate valid code involves a lot of steps. This change includes the initial version of all of these steps, but leaves a lot of gaps where more complete implementation is required. The changes include: - Member lookup on types has been centralized, and now handles the case where the type we are looking for a member in is a generic parameter (e.g., given `t.help()` we can now look up `help` in `Helper` by knowing that `t` is a `T` and `T` conforms to `Helper`). - There is an obvious cleanup still to be done here where the same exact logic should be used to look up available "constructor" declarations inside a type when the type is used like a function. - Add a notion of subtype constraint "wittnesses" to the type system. When a generic is declared as taking `<T : Helper>` it really takes two generic parameters: the type `T` and a proof that `T` conforms to `Helper`. The actual arguments to a generic will then include both the type argument and a suitable witness argument (both type-level values). - As it stands right now, a witness wraps a `DeclRef` to the declaration that represents the appropriate subtype relationship. So if we have `struct A : Helper`, that `: Helper` part turns into an `InheritanceDecl` member, and a reference to that member can serve as a witness to the fact that `A` conforms to `Helper`. - Make explicit generic application `G<A,B>` synthesize the additional arguments that represent conformances required by the generic. - This does *not* yet deal with the case where a generic is implicitly specialized as part of an ordinary call `G(a,b)` - A bug fix to not auto-specialize generics during lookup. The problem here was related to an attempted fix of an earlier issue. During checking of a method nested in a generic type, we were running into problems where `DeclRefType::create()` was getting called on an un-specialized reference to `vector`, and this was leading to a crash when the code looked for the arguments for the generic. This was worked around by having name lookup automatically specialize any generics it runs into while going through lookup contexts. That choice creates the problem that in a generic method like this: ``` void test<T>(T val) { ... } ``` any reference to `val` inside the body of `test` will end up getting specialized so that it is effectively `test<T>::val`, when that isn't really needed. - Add front-end logic to check that when a type claims to conform to an interface it actually must provide the methods required by the interface. The checking process goes ahead and builds a front-end "witness table" that maps declarations in the interface being conformed to over to their concrete implementations for the type. - At the moment the checking is completely broken and bad: it assumes that *any* member with the right name is an appropriate declaration to satisfy a requirement. That obviously needs to be fixed. - Add an explicit operation to the IR for lookup of methods: `lookup_interface_method(w, r)` where `w` is a reference to the "witness" value and `r` is an `IRDeclRef` for the member we want to look up. - Add an explicit notion of witness tables to the IR. These end up being the IR representation of an `InheritanceDecl` in a type, and they are generated by enumerating the members that satisfy the interface requirements (which were handily already enumerated by the front-end checking). The witness table is an explicit IR value, and so it will be referenced/used at the site where conformance is being exploited (e.g., as part of a `specialize` call), so it should be safe to eliminate witness tables that are unused (since they represent conformances that aren't actually exploited). Similarly, the entries in a witness table are uses of the functions that implement interface methods, and so keep those live. - In order to implement the above, I did a bit of a cleanup pass on the IR representation so that there is an `IRUser` base that `IRInst` inherits from, so that we can have users of values that aren't instructions. - One annoying thing is that because of how types and generics are handled in the IR, we needed a way to have a type-level `Val` that wraps an IR-level value: e.g., to allow an IR-level witness table to be used as one of the arguments for specialization of a generic. The design I chose here is to have a "proxy" `Val` subclass (`IRProxyVal`) that wraps an `IRValue*`. These should only ever appear as part of types and `DeclRef`s that are used by the IR. - One annoying bit here is that an IR value might then have a use that is not manifest in the set of IR instructions, and instead only appears as part of a type somewhere. - I'm not 100% happy with this design, but it seems like we'd have to tackle similar issues if/when we eventually allow functions to have `constexpr` or `@Constant` parameters - Make generic specialization also propagate witness table arguments through to their use sites (this is mostly just the existing substitution machinery, once we have `IRProxyVal`), and then include logic to specialize `lookup_interface_method` instructions when their first operand is a concrete witness table. All of this work allows a single limited test using generics with constraints to pass, but more work is needed to make the solution robust.
1 parent 56bc826 commit 4ab545b

24 files changed

+2156
-484
lines changed

source/slang/bytecode.cpp

+3-3
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ BytecodeGenerationPtr<BCSymbol> generateBytecodeSymbolForInst(
728728
paramCount++;
729729
}
730730

731-
for( auto ii = bb->getFirstInst(); ii; ii = ii->nextInst )
731+
for( auto ii = bb->getFirstInst(); ii; ii = ii->getNextInst() )
732732
{
733733
switch( ii->op )
734734
{
@@ -792,7 +792,7 @@ BytecodeGenerationPtr<BCSymbol> generateBytecodeSymbolForInst(
792792

793793
// Now loop over the non-parameter instructions and
794794
// allocate actual register locations to them.
795-
for( auto ii = bb->getFirstInst(); ii; ii = ii->nextInst )
795+
for( auto ii = bb->getFirstInst(); ii; ii = ii->getNextInst() )
796796
{
797797
switch(ii->op)
798798
{
@@ -857,7 +857,7 @@ BytecodeGenerationPtr<BCSymbol> generateBytecodeSymbolForInst(
857857
UInt blockOffset = subContext->currentBytecode.Count();
858858
blockOffsets.Add( blockOffset );
859859

860-
for( auto ii = bb->getFirstInst(); ii; ii = ii->nextInst )
860+
for( auto ii = bb->getFirstInst(); ii; ii = ii->getNextInst() )
861861
{
862862
// What we do with each instruction depends a bit on the
863863
// kind of instruction it is.

source/slang/check.cpp

+671-138
Large diffs are not rendered by default.

source/slang/decl-defs.h

+9
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ SIMPLE_SYNTAX_CLASS(InterfaceDecl, AggTypeDecl)
9696
SYNTAX_CLASS(InheritanceDecl, Decl)
9797
// The type expression as written
9898
SYNTAX_FIELD(TypeExp, base)
99+
100+
RAW(
101+
// After checking, this dictionary will map members
102+
// required by the base type to their concrete
103+
// implementations in the type that contains
104+
// this inheritance declaration.
105+
Dictionary<DeclRef<Decl>, Decl*> requirementWitnesses;
106+
)
107+
99108
END_SYNTAX_CLASS()
100109

101110
// TODO: may eventually need sub-classes for explicit/direct vs. implicit/indirect inheritance

source/slang/diagnostic-defs.h

+2
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@ DIAGNOSTIC(38001, Error, ambiguousEntryPoint, "more than one funct
316316
DIAGNOSTIC(38002, Note, entryPointCandidate, "see candidate declaration for entry point '$0'")
317317
DIAGNOSTIC(38003, Error, entryPointSymbolNotAFunction, "entry point '$0' must be declared as a function")
318318

319+
DIAGNOSTIC(38100, Error, typeDoesntImplementInterfaceRequirement, "type '$0' does not provide required interface member '$1'")
320+
319321
//
320322
// 4xxxx - IL code generation.
321323
//

source/slang/emit.cpp

+43-103
Original file line numberDiff line numberDiff line change
@@ -5172,7 +5172,7 @@ emitDeclImpl(decl, nullptr);
51725172
// Start by emitting the non-terminator instructions in the block.
51735173
auto terminator = block->getLastInst();
51745174
assert(isTerminatorInst(terminator));
5175-
for (auto inst = block->getFirstInst(); inst != terminator; inst = inst->nextInst)
5175+
for (auto inst = block->getFirstInst(); inst != terminator; inst = inst->getNextInst())
51765176
{
51775177
emitIRInst(context, inst);
51785178
}
@@ -5508,6 +5508,26 @@ emitDeclImpl(decl, nullptr);
55085508
EmitContext* context,
55095509
IRFunc* func)
55105510
{
5511+
// We don't want to declare generic functions,
5512+
// because none of our targets actually support them.
5513+
if(func->genericDecl)
5514+
return;
5515+
5516+
// We also don't want to emit declarations for operations
5517+
// that only appear in the IR as stand-ins for built-in
5518+
// operations on that target.
5519+
if (isTargetIntrinsic(context, func))
5520+
return;
5521+
5522+
// Finally, don't emit a declaration for an entry point,
5523+
// because it might need meta-data attributes attached
5524+
// to it, and the HLSL compiler will get upset if the
5525+
// forward declaration doesn't *also* have those
5526+
// attributes.
5527+
if(asEntryPoint(func))
5528+
return;
5529+
5530+
55115531
// A function declaration doesn't have any IR basic blocks,
55125532
// and as a result it *also* doesn't have the IR `param` instructions,
55135533
// so we need to emit a declaration entirely from the type.
@@ -5566,82 +5586,6 @@ emitDeclImpl(decl, nullptr);
55665586
return nullptr;
55675587
}
55685588

5569-
#if 0
5570-
void emitGLSLEntryPointFunc(
5571-
EmitContext* context,
5572-
IRFunc* func)
5573-
{
5574-
auto funcType = func->getType();
5575-
auto resultType = func->getResultType();
5576-
5577-
auto entryPointLayout = getEntryPointLayout(context, func);
5578-
assert(entryPointLayout);
5579-
5580-
// TODO: need to deal with decorations on the entry point
5581-
// that should be turned into global-scope `layout` qualifiers.
5582-
5583-
// TODO: emit kernel inputs and outputs to globals.
5584-
5585-
// Emit a global `out` declaration to hold the output from our shader
5586-
// kernel.
5587-
//
5588-
// TODO: need to generate unique names beter than this
5589-
//
5590-
// TODO: need to handle the case where the output is
5591-
// a structure (should that be fixed up at the IR level,
5592-
// or here?).
5593-
// Best option might be to translate the entry-point
5594-
// result parameter into an `out` parameter, so that
5595-
// we can handle those uniformly.
5596-
//
5597-
String resultName = getIRName(func) + "_result";
5598-
emitGLSLLayoutQualifiers(entryPointLayout->resultLayout);
5599-
emit("out ");
5600-
emitIRType(context, resultType, resultName);
5601-
emit(";\n");
5602-
5603-
// Emit global `in` and/or `out` declarations for the
5604-
// parameters of our shader kernel.
5605-
//
5606-
// TODO: We need to make sure these names don't collide with anything.
5607-
//
5608-
// TODO: We need to handle scalarization here.
5609-
//
5610-
auto firstParam = func->getFirstParam();
5611-
for( auto pp = firstParam; pp; pp = pp->getNextParam() )
5612-
{
5613-
// TODO: actually handle `out` parameters here.
5614-
5615-
auto paramLayout = getVarLayout(context, pp);
5616-
auto paramName = getIRName(pp);
5617-
emitGLSLLayoutQualifiers(paramLayout);
5618-
emit("in ");
5619-
emitIRType(context, pp->getType(), paramName);
5620-
emit(";\n");
5621-
}
5622-
5623-
// Now that we've emitted our parameter declarations,
5624-
// we can start to emit the body of the entry point:
5625-
//
5626-
emit("void main()\n{\n");
5627-
5628-
// We had better not be trying to output an entry
5629-
// point from a declaration rather than a definition.
5630-
assert(isDefinition(func));
5631-
5632-
// At the most basic, we just want to emit the operations in
5633-
// the entry point function directly, but with the small catch
5634-
// that if there was a `return` statement in there somewhere,
5635-
// we need to turn that into a write to our output variable.
5636-
//
5637-
// TODO: yeah, that should get cleared up at the IR level...
5638-
//
5639-
emitIRStmtsForBlocks(context, func->getFirstBlock(), nullptr);
5640-
5641-
emit("}\n");
5642-
}
5643-
#endif
5644-
56455589
EntryPointLayout* asEntryPoint(IRFunc* func)
56465590
{
56475591
if (auto layoutDecoration = func->findDecoration<IRLayoutDecoration>())
@@ -5697,26 +5641,11 @@ emitDeclImpl(decl, nullptr);
56975641
EmitContext* context,
56985642
IRFunc* func)
56995643
{
5700-
#if 0
5701-
if( getTarget(context) == CodeGenTarget::GLSL
5702-
&& isEntryPoint(func) )
5703-
{
5704-
// We have a shader entry point, and that
5705-
// requires a different strategy for source
5706-
// code generation in GLSL, because the
5707-
// parameters/result of the entry point
5708-
// need to be translated into globals.
5709-
//
5710-
5711-
emitGLSLEntryPointFunc(context, func);
5712-
}
5713-
else
5714-
#endif
57155644
if(func->genericDecl)
57165645
{
57175646
Emit("/* ");
57185647
emitIRFuncDecl(context, func);
5719-
Emit(" */");
5648+
Emit(" */\n");
57205649
return;
57215650
}
57225651

@@ -6129,12 +6058,6 @@ emitDeclImpl(decl, nullptr);
61296058
emitIRGlobalVar(context, (IRGlobalVar*) inst);
61306059
break;
61316060

6132-
#if 0
6133-
case kIROp_StructType:
6134-
emitIRStruct(context, (IRStructDecl*) inst);
6135-
break;
6136-
#endif
6137-
61386061
case kIROp_Var:
61396062
emitIRVar(context, (IRVar*) inst);
61406063
break;
@@ -6257,7 +6180,7 @@ emitDeclImpl(decl, nullptr);
62576180
emitIRUsedTypesForValue(context, pp);
62586181
}
62596182

6260-
for( auto ii = bb->getFirstInst(); ii; ii = ii->nextInst )
6183+
for( auto ii = bb->getFirstInst(); ii; ii = ii->getNextInst() )
62616184
{
62626185
emitIRUsedTypesForValue(context, ii);
62636186
}
@@ -6289,6 +6212,20 @@ emitDeclImpl(decl, nullptr);
62896212
{
62906213
emitIRUsedTypesForModule(context, module);
62916214

6215+
// Before we emit code, we need to forward-declare
6216+
// all of our functions so that we don't have to
6217+
// sort them by dependencies.
6218+
for( auto gv = module->getFirstGlobalValue(); gv; gv = gv->getNextValue() )
6219+
{
6220+
if(gv->op != kIROp_Func)
6221+
continue;
6222+
6223+
auto func = (IRFunc*) gv;
6224+
emitIRFuncDecl(context, func);
6225+
}
6226+
6227+
6228+
62926229
for( auto gv = module->getFirstGlobalValue(); gv; gv = gv->getNextValue() )
62936230
{
62946231
emitIRGlobalInst(context, gv);
@@ -6454,9 +6391,12 @@ String emitEntryPoint(
64546391

64556392
specializeGenerics(lowered);
64566393

6457-
// fprintf(stderr, "###\n");
6458-
// dumpIR(lowered);
6459-
// fprintf(stderr, "###\n");
6394+
// Debugging code for IR transformations...
6395+
#if 0
6396+
fprintf(stderr, "###\n");
6397+
dumpIR(lowered);
6398+
fprintf(stderr, "###\n");
6399+
#endif
64606400

64616401
//
64626402
// TODO: Need to decide whether to do these before or after

source/slang/ir-inst-defs.h

+3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ INST(FloatLit, float_constant, 0, 0)
9797
INST(decl_ref, decl_ref, 0, 0)
9898

9999
INST(specialize, specialize, 2, 0)
100+
INST(lookup_interface_method, lookup_interface_method, 2, 0)
100101

101102
INST(Construct, construct, 0, 0)
102103
INST(Call, call, 1, 0)
@@ -106,6 +107,8 @@ INST(Func, func, 0, PARENT)
106107
INST(Block, block, 0, PARENT)
107108

108109
INST(global_var, global_var, 0, 0)
110+
INST(witness_table, witness_table, 0, 0)
111+
INST(witness_table_entry, witness_table_entry, 2, 0)
109112

110113
INST(Param, param, 0, 0)
111114
INST(StructField, field, 0, 0)

source/slang/ir-insts.h

+45
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ struct IRSpecialize : IRInst
7474
IRUse specDeclRefVal;
7575
};
7676

77+
// An instruction that looks up the implementation
78+
// of an interface operation identified by `requirementDeclRef`
79+
// in the witness table `witnessTable` which should
80+
// hold the conformance information for a specific type.
81+
struct IRLookupWitnessMethod : IRInst
82+
{
83+
IRUse witnessTable;
84+
IRUse requirementDeclRef;
85+
};
86+
7787
//
7888

7989
struct IRCall : IRInst
@@ -251,6 +261,26 @@ struct IRGlobalVar : IRGlobalValue
251261
PtrType* getType() { return type.As<PtrType>(); }
252262
};
253263

264+
// An entry in a witness table (see below)
265+
struct IRWitnessTableEntry : IRUser
266+
{
267+
// The AST-level requirement
268+
IRUse requirementKey;
269+
270+
// The IR-level value that satisfies the requirement
271+
IRUse satisfyingVal;
272+
};
273+
274+
// A witness table is a global value that stores
275+
// information about how a type conforms to some
276+
// interface. It basically takes the form of a
277+
// map from the required members of the interface
278+
// to the IR values that satisfy those requirements.
279+
struct IRWitnessTable : IRGlobalValue
280+
{
281+
IRValueList<IRWitnessTableEntry> entries;
282+
};
283+
254284

255285

256286
// Description of an instruction to be used for global value numbering
@@ -330,6 +360,16 @@ struct IRBuilder
330360
IRValue* genericVal,
331361
DeclRef<Decl> specDeclRef);
332362

363+
IRValue* emitLookupInterfaceMethodInst(
364+
IRType* type,
365+
IRValue* witnessTableVal,
366+
IRValue* interfaceMethodVal);
367+
368+
IRValue* emitLookupInterfaceMethodInst(
369+
IRType* type,
370+
DeclRef<Decl> witnessTableDeclRef,
371+
DeclRef<Decl> interfaceMethodDeclRef);
372+
333373
IRInst* emitCallInst(
334374
IRType* type,
335375
IRValue* func,
@@ -352,6 +392,11 @@ struct IRBuilder
352392
IRFunc* createFunc();
353393
IRGlobalVar* createGlobalVar(
354394
IRType* valueType);
395+
IRWitnessTable* createWitnessTable();
396+
IRWitnessTableEntry* createWitnessTableEntry(
397+
IRWitnessTable* witnessTable,
398+
IRValue* requirementKey,
399+
IRValue* satisfyingVal);
355400

356401
IRBlock* createBlock();
357402
IRBlock* emitBlock();

0 commit comments

Comments
 (0)