// slang-ir-bind-existentials.cpp #include "slang-ir-bind-existentials.h" #include "slang-ir-insts.h" #include "slang-ir.h" namespace Slang { // The code that comes out of the linking step will have instructions added // that indicate how parameters with existential (interface) types are supposed // to be specialized to concrete types. // // If there are any global existential-type parameters there should be a // `bindGlobalExistentialSlots(...)` instruction at module scope. // // For each entry point with entry-point existential parameters, there should // be a `[bindExistentialSlots(...)]` decoration attached to the entry // point itself. // // In each case, the operands of the instruction should be a sequence of // pairs. The number of pairs should match the number of existential "slots" // at global or entry-point scope. Each pair should comprise a type `T` // to plug into the slot, and a witness table `w` for the conformance of // `T` to the interface type in that slot. // // In the simplest case, if we have a global shader parameter of interface // type: // // IFoo p; // // Then this will lower to the IR as: // // global_param p : IFoo; // // And if the user tries to specialize `p` to type `Bar`, and a witness // table `bar_is_ifoo`, we've have: // // bindGlobalExistentialSlots(Bar, bar_is_ifoo); // // The goal of this pass is to replace the parameter of interface type // with one of concrete type: // // global_param p_new : Bar; // // and replace any reference to the old `p` parameter with // a `makeExistential(p_new, bar_is_ifoo)`. That preserves the // fact that a reference to `p` is conceptually of type `IFoo`, // but allows downstream optimization passes to start specializing // code based on the concrete knowledge that the value "backing" // the parameter is actaully of type `Bar`. // As is typically for IR passes, we will encapsulate all the // logic in a `struct` type. // struct BindExistentialSlots { IRModule* module = nullptr; DiagnosticSink* sink = nullptr; void processModule() { // We will start by dealing with the global existential slots. processGlobalExistentialSlots(); // Then we will process the per-entry-point existential slots. processEntryPointExistentialSlots(); } void processGlobalExistentialSlots() { // We will search for global shader parameters that make // use of existential specialization parameters. // for (auto inst : module->getGlobalInsts()) { // We only care about global shader parameters. // auto globalParam = as(inst); if (!globalParam) continue; // We only care about global shader parameters // that have existential specialization parameters, // and we expect all such parameters to have a // `[bindExistentialSlots(...)]` decoration that // was added during IR linking. // auto bindSlotsInst = globalParam->findDecorationImpl(kIROp_BindExistentialSlotsDecoration); if (!bindSlotsInst) continue; replaceTypeUsingExistentialSlots( globalParam, bindSlotsInst->getOperandCount(), bindSlotsInst->getOperands()); // Once we have propagated the information from // the `[bindExistentialSlots(...)]` decoration // down into the parameter's type, we no longer // need the decoration. // bindSlotsInst->removeAndDeallocate(); } } void processEntryPointExistentialSlots() { // The overall flow for the entry-point case is similar // to the global case. // // We start by iterating over all the functions at // global scope and look for entry points. // for (auto inst : module->getGlobalInsts()) { auto func = as(inst); if (!func) continue; if (!func->findDecorationImpl(kIROp_EntryPointDecoration)) continue; // We then process each entry point we find. // processEntryPointExistentialSlots(func); } } void processEntryPointExistentialSlots(IRFunc* func) { // When looking at a single `func`, we need // to find the `[bindExistentialSlots(...)]` decoration, // if it has one. // auto bindEntryPointExistentialSlotsInst = func->findDecorationImpl(kIROp_BindExistentialSlotsDecoration); // We then need to process each of the entry-point // parameters just like we did for global parameters. // // Because the existential slot arguments for *all* of the parameters // are attached in a single `[bindExistentialSlots(...)]` decoration, // we need to carve them up appropriately across the parameters. // The way we do this is a bit of a kludge, in that we track a // single `slotOffset` and increment it for each parameter by the // number of arguments it consumed. // // Note: a better approach here might rely on the layout information // for the parameters, which should directly encode an offset for // the existential specialization parameters it uses. The challenge // with this is that we'd need to correctly interpret the offset // relative to any global-scope specialization parameters or // generic specialization parameters of the entry point. // Ultimately the simplistic counter approach is less complicated. // Index slotOffset = 0; for (auto param : func->getParams()) { processEntryPointParameter(param, bindEntryPointExistentialSlotsInst, slotOffset); } // TODO: We would need to consider what to do if // we had an existential return type for `func`. // // In general, it probably doesn't make sense to // have existential types in varying input/output // at all, so the front-end should probably be // validating that. // Once we've processed all the parameters, the information // in the `[bindExistentialSlots(...)]` decoration is // no longer needed, and we can remove it. // if (bindEntryPointExistentialSlotsInst) { bindEntryPointExistentialSlotsInst->removeAndDeallocate(); } } // When processing a single parameter we need to have access // to the corresponding instruction that will bind its slots. // // We don't care whether we have a `global_param` and a // `bindGlobalExistentialSlots` instruction, or an entry-point // function `param` and a `[bindExistentialSlots(...)]` // decoration; both use the same subroutine. // void processEntryPointParameter( IRInst* param, IRInst* bindSlotsInst, Index& ioSlotOperandOffset) { // We expect all shader parameters to have layout information, // but to be defensive we will skip any that don't. // auto layoutDecoration = param->findDecoration(); if (!layoutDecoration) return; auto varLayout = as(layoutDecoration->getLayout()); if (!varLayout) return; // We only care about parameters that are associated // with one or more existential slots. // auto resInfo = varLayout->findOffsetAttr(LayoutResourceKind::ExistentialTypeParam); if (!resInfo) return; // We will use the layout information on the variable to // find out the stating slot, and the information on // the type to find out the number of slots. // UInt slotCount = 0; if (auto typeResInfo = varLayout->getTypeLayout()->findSizeAttr(LayoutResourceKind::ExistentialTypeParam)) slotCount = UInt(typeResInfo->getFiniteSize()); // At this point we know that the parameter consumes // some number of slots, so it would be an error // if we don't have an instruction to bind the slots. // if (!bindSlotsInst) { // Note: This error is considered an internal error because // we should be detecting and diagnosing this problem before // we make it to back-end code generation. // sink->diagnose(param->sourceLoc, Diagnostics::missingExistentialBindingsForParameter); return; } // Each existential slot corresponds to *two* arguments // on the binding instruction: one for the type, and // another for the witness table. // // We will check to make sure we have enough operands to cover // this parameter. // UInt bindOperandCount = bindSlotsInst->getOperandCount(); UInt slotOperandCount = 2 * slotCount; if ((ioSlotOperandOffset + slotOperandCount) > bindOperandCount) { sink->diagnose(param->sourceLoc, Diagnostics::missingExistentialBindingsForParameter); return; } // // If there are enough operands, then we will offset to // get to the starting point for the current parameter, // keeping in mind that each slot accounts for two // operands. // auto operandsForInst = bindSlotsInst->getOperands() + ioSlotOperandOffset; // Once we've found the operands that are relevent to // the slots used by `param`, we will defer to a routine // that replaces the type of `param` based on the // information in the slots. // replaceTypeUsingExistentialSlots(param, slotOperandCount, operandsForInst); ioSlotOperandOffset += slotOperandCount; } void replaceTypeUsingExistentialSlots( IRInst* inst, UInt slotOperandCount, IRUse const* slotArgs) { // We are going to alter the type of the // given `inst` based on information in // the `slotArgs`. auto fullType = inst->getFullType(); IRBuilder builder(module); // Every argument that is filling an existential // type param/slot comprises both a type and // a witness table, so the total number of operands // is twice the number of slots we are filling. // List slotOperands; for (UInt ii = 0; ii < slotOperandCount; ++ii) slotOperands.add(slotArgs[ii].get()); // We are going to create a proxy type that represents // the results of plugging all the information // from the existential slots into the original type. // auto newType = builder.getBindExistentialsType(fullType, slotOperandCount, slotOperands.getBuffer()); // We will replace the type of the original parameter // with the new proxy type. // builder.setDataType(inst, newType); // Next we want to replace all uses of `inst` (which // expect a value of its old type) with a fresh // `wrapExistential(...)` instruction that refers to // `inst` with its new type. // // Note: we make a copy of the list of uses for `inst` // before going through and replacing them, because // during the replacement we make *more* uses of `inst`, // as an operand to the `makeExistential` instructions. // We only want to replace the old uses, and not the // new ones we'll be making. // List usesToReplace; for (auto use = inst->firstUse; use; use = use->nextUse) { auto user = use->getUser(); // Note: We don't want to replace uses that are // just referring to an instruction to identify // it (e.g., a global shader parameter). We enumerate // the relevant cases here and skip them. // if (as(user)) continue; if (as(user)) continue; if (as(user)) continue; usesToReplace.add(use); } // Now we can loop over our list of uses and replace each. // for (auto use : usesToReplace) { // We are going to emit a `wrapExistential` (or `makeExistential`) // right before each use site of the value. // builder.setInsertBefore(use->getUser()); // The `inst` used to have an existential/interface type, // but will now have a concrete/bound type, and we need // to wrap it up again to get a value of the original // expected type. // auto newVal = builder.emitWrapExistential( fullType, inst, slotOperandCount, slotOperands.getBuffer()); builder.replaceOperand(use, newVal); } } }; void bindExistentialSlots(IRModule* module, DiagnosticSink* sink) { BindExistentialSlots context; context.module = module; context.sink = sink; context.processModule(); } } // namespace Slang