xref: /llvm-project/clang/lib/Sema/SemaHLSL.cpp (revision 41f0574c4654fb8a8cbb8c26d453f51a31cfd2a0)
1782ac218SXiang Li //===- SemaHLSL.cpp - Semantic Analysis for HLSL constructs ---------------===//
2782ac218SXiang Li //
3782ac218SXiang Li // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4782ac218SXiang Li // See https://llvm.org/LICENSE.txt for license information.
5782ac218SXiang Li // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6782ac218SXiang Li //
7782ac218SXiang Li //===----------------------------------------------------------------------===//
8782ac218SXiang Li // This implements Semantic Analysis for HLSL constructs.
9782ac218SXiang Li //===----------------------------------------------------------------------===//
10782ac218SXiang Li 
11d345f6a2SVlad Serebrennikov #include "clang/Sema/SemaHLSL.h"
12b45c9c31SVlad Serebrennikov #include "clang/Basic/DiagnosticSema.h"
13b45c9c31SVlad Serebrennikov #include "clang/Basic/LLVM.h"
14b45c9c31SVlad Serebrennikov #include "clang/Basic/TargetInfo.h"
15782ac218SXiang Li #include "clang/Sema/Sema.h"
16b45c9c31SVlad Serebrennikov #include "llvm/ADT/STLExtras.h"
17b45c9c31SVlad Serebrennikov #include "llvm/ADT/StringExtras.h"
18b45c9c31SVlad Serebrennikov #include "llvm/ADT/StringRef.h"
19b45c9c31SVlad Serebrennikov #include "llvm/Support/ErrorHandling.h"
20b45c9c31SVlad Serebrennikov #include "llvm/TargetParser/Triple.h"
21b45c9c31SVlad Serebrennikov #include <iterator>
22782ac218SXiang Li 
23782ac218SXiang Li using namespace clang;
24782ac218SXiang Li 
25d345f6a2SVlad Serebrennikov SemaHLSL::SemaHLSL(Sema &S) : SemaBase(S) {}
26d345f6a2SVlad Serebrennikov 
27b45c9c31SVlad Serebrennikov Decl *SemaHLSL::ActOnStartBuffer(Scope *BufferScope, bool CBuffer,
28b45c9c31SVlad Serebrennikov                                  SourceLocation KwLoc, IdentifierInfo *Ident,
29782ac218SXiang Li                                  SourceLocation IdentLoc,
30782ac218SXiang Li                                  SourceLocation LBrace) {
31782ac218SXiang Li   // For anonymous namespace, take the location of the left brace.
32d345f6a2SVlad Serebrennikov   DeclContext *LexicalParent = SemaRef.getCurLexicalContext();
33782ac218SXiang Li   HLSLBufferDecl *Result = HLSLBufferDecl::Create(
34d345f6a2SVlad Serebrennikov       getASTContext(), LexicalParent, CBuffer, KwLoc, Ident, IdentLoc, LBrace);
35782ac218SXiang Li 
36d345f6a2SVlad Serebrennikov   SemaRef.PushOnScopeChains(Result, BufferScope);
37d345f6a2SVlad Serebrennikov   SemaRef.PushDeclContext(BufferScope, Result);
38782ac218SXiang Li 
39782ac218SXiang Li   return Result;
40782ac218SXiang Li }
41782ac218SXiang Li 
42*41f0574cSXiang Li // Calculate the size of a legacy cbuffer type based on
43*41f0574cSXiang Li // https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-packing-rules
44*41f0574cSXiang Li static unsigned calculateLegacyCbufferSize(const ASTContext &Context,
45*41f0574cSXiang Li                                            QualType T) {
46*41f0574cSXiang Li   unsigned Size = 0;
47*41f0574cSXiang Li   constexpr unsigned CBufferAlign = 128;
48*41f0574cSXiang Li   if (const RecordType *RT = T->getAs<RecordType>()) {
49*41f0574cSXiang Li     const RecordDecl *RD = RT->getDecl();
50*41f0574cSXiang Li     for (const FieldDecl *Field : RD->fields()) {
51*41f0574cSXiang Li       QualType Ty = Field->getType();
52*41f0574cSXiang Li       unsigned FieldSize = calculateLegacyCbufferSize(Context, Ty);
53*41f0574cSXiang Li       unsigned FieldAlign = 32;
54*41f0574cSXiang Li       if (Ty->isAggregateType())
55*41f0574cSXiang Li         FieldAlign = CBufferAlign;
56*41f0574cSXiang Li       Size = llvm::alignTo(Size, FieldAlign);
57*41f0574cSXiang Li       Size += FieldSize;
58*41f0574cSXiang Li     }
59*41f0574cSXiang Li   } else if (const ConstantArrayType *AT = Context.getAsConstantArrayType(T)) {
60*41f0574cSXiang Li     if (unsigned ElementCount = AT->getSize().getZExtValue()) {
61*41f0574cSXiang Li       unsigned ElementSize =
62*41f0574cSXiang Li           calculateLegacyCbufferSize(Context, AT->getElementType());
63*41f0574cSXiang Li       unsigned AlignedElementSize = llvm::alignTo(ElementSize, CBufferAlign);
64*41f0574cSXiang Li       Size = AlignedElementSize * (ElementCount - 1) + ElementSize;
65*41f0574cSXiang Li     }
66*41f0574cSXiang Li   } else if (const VectorType *VT = T->getAs<VectorType>()) {
67*41f0574cSXiang Li     unsigned ElementCount = VT->getNumElements();
68*41f0574cSXiang Li     unsigned ElementSize =
69*41f0574cSXiang Li         calculateLegacyCbufferSize(Context, VT->getElementType());
70*41f0574cSXiang Li     Size = ElementSize * ElementCount;
71*41f0574cSXiang Li   } else {
72*41f0574cSXiang Li     Size = Context.getTypeSize(T);
73*41f0574cSXiang Li   }
74*41f0574cSXiang Li   return Size;
75*41f0574cSXiang Li }
76*41f0574cSXiang Li 
77b45c9c31SVlad Serebrennikov void SemaHLSL::ActOnFinishBuffer(Decl *Dcl, SourceLocation RBrace) {
78782ac218SXiang Li   auto *BufDecl = cast<HLSLBufferDecl>(Dcl);
79782ac218SXiang Li   BufDecl->setRBraceLoc(RBrace);
80*41f0574cSXiang Li 
81*41f0574cSXiang Li   // Validate packoffset.
82*41f0574cSXiang Li   llvm::SmallVector<std::pair<VarDecl *, HLSLPackOffsetAttr *>> PackOffsetVec;
83*41f0574cSXiang Li   bool HasPackOffset = false;
84*41f0574cSXiang Li   bool HasNonPackOffset = false;
85*41f0574cSXiang Li   for (auto *Field : BufDecl->decls()) {
86*41f0574cSXiang Li     VarDecl *Var = dyn_cast<VarDecl>(Field);
87*41f0574cSXiang Li     if (!Var)
88*41f0574cSXiang Li       continue;
89*41f0574cSXiang Li     if (Field->hasAttr<HLSLPackOffsetAttr>()) {
90*41f0574cSXiang Li       PackOffsetVec.emplace_back(Var, Field->getAttr<HLSLPackOffsetAttr>());
91*41f0574cSXiang Li       HasPackOffset = true;
92*41f0574cSXiang Li     } else {
93*41f0574cSXiang Li       HasNonPackOffset = true;
94*41f0574cSXiang Li     }
95*41f0574cSXiang Li   }
96*41f0574cSXiang Li 
97*41f0574cSXiang Li   if (HasPackOffset && HasNonPackOffset)
98*41f0574cSXiang Li     Diag(BufDecl->getLocation(), diag::warn_hlsl_packoffset_mix);
99*41f0574cSXiang Li 
100*41f0574cSXiang Li   if (HasPackOffset) {
101*41f0574cSXiang Li     ASTContext &Context = getASTContext();
102*41f0574cSXiang Li     // Make sure no overlap in packoffset.
103*41f0574cSXiang Li     // Sort PackOffsetVec by offset.
104*41f0574cSXiang Li     std::sort(PackOffsetVec.begin(), PackOffsetVec.end(),
105*41f0574cSXiang Li               [](const std::pair<VarDecl *, HLSLPackOffsetAttr *> &LHS,
106*41f0574cSXiang Li                  const std::pair<VarDecl *, HLSLPackOffsetAttr *> &RHS) {
107*41f0574cSXiang Li                 return LHS.second->getOffset() < RHS.second->getOffset();
108*41f0574cSXiang Li               });
109*41f0574cSXiang Li 
110*41f0574cSXiang Li     for (unsigned i = 0; i < PackOffsetVec.size() - 1; i++) {
111*41f0574cSXiang Li       VarDecl *Var = PackOffsetVec[i].first;
112*41f0574cSXiang Li       HLSLPackOffsetAttr *Attr = PackOffsetVec[i].second;
113*41f0574cSXiang Li       unsigned Size = calculateLegacyCbufferSize(Context, Var->getType());
114*41f0574cSXiang Li       unsigned Begin = Attr->getOffset() * 32;
115*41f0574cSXiang Li       unsigned End = Begin + Size;
116*41f0574cSXiang Li       unsigned NextBegin = PackOffsetVec[i + 1].second->getOffset() * 32;
117*41f0574cSXiang Li       if (End > NextBegin) {
118*41f0574cSXiang Li         VarDecl *NextVar = PackOffsetVec[i + 1].first;
119*41f0574cSXiang Li         Diag(NextVar->getLocation(), diag::err_hlsl_packoffset_overlap)
120*41f0574cSXiang Li             << NextVar << Var;
121*41f0574cSXiang Li       }
122*41f0574cSXiang Li     }
123*41f0574cSXiang Li   }
124*41f0574cSXiang Li 
125d345f6a2SVlad Serebrennikov   SemaRef.PopDeclContext();
126782ac218SXiang Li }
127b45c9c31SVlad Serebrennikov 
128b45c9c31SVlad Serebrennikov HLSLNumThreadsAttr *SemaHLSL::mergeNumThreadsAttr(Decl *D,
129b45c9c31SVlad Serebrennikov                                                   const AttributeCommonInfo &AL,
130b45c9c31SVlad Serebrennikov                                                   int X, int Y, int Z) {
131b45c9c31SVlad Serebrennikov   if (HLSLNumThreadsAttr *NT = D->getAttr<HLSLNumThreadsAttr>()) {
132b45c9c31SVlad Serebrennikov     if (NT->getX() != X || NT->getY() != Y || NT->getZ() != Z) {
133b45c9c31SVlad Serebrennikov       Diag(NT->getLocation(), diag::err_hlsl_attribute_param_mismatch) << AL;
134b45c9c31SVlad Serebrennikov       Diag(AL.getLoc(), diag::note_conflicting_attribute);
135b45c9c31SVlad Serebrennikov     }
136b45c9c31SVlad Serebrennikov     return nullptr;
137b45c9c31SVlad Serebrennikov   }
138b45c9c31SVlad Serebrennikov   return ::new (getASTContext())
139b45c9c31SVlad Serebrennikov       HLSLNumThreadsAttr(getASTContext(), AL, X, Y, Z);
140b45c9c31SVlad Serebrennikov }
141b45c9c31SVlad Serebrennikov 
142b45c9c31SVlad Serebrennikov HLSLShaderAttr *
143b45c9c31SVlad Serebrennikov SemaHLSL::mergeShaderAttr(Decl *D, const AttributeCommonInfo &AL,
144b45c9c31SVlad Serebrennikov                           HLSLShaderAttr::ShaderType ShaderType) {
145b45c9c31SVlad Serebrennikov   if (HLSLShaderAttr *NT = D->getAttr<HLSLShaderAttr>()) {
146b45c9c31SVlad Serebrennikov     if (NT->getType() != ShaderType) {
147b45c9c31SVlad Serebrennikov       Diag(NT->getLocation(), diag::err_hlsl_attribute_param_mismatch) << AL;
148b45c9c31SVlad Serebrennikov       Diag(AL.getLoc(), diag::note_conflicting_attribute);
149b45c9c31SVlad Serebrennikov     }
150b45c9c31SVlad Serebrennikov     return nullptr;
151b45c9c31SVlad Serebrennikov   }
152b45c9c31SVlad Serebrennikov   return HLSLShaderAttr::Create(getASTContext(), ShaderType, AL);
153b45c9c31SVlad Serebrennikov }
154b45c9c31SVlad Serebrennikov 
155b45c9c31SVlad Serebrennikov HLSLParamModifierAttr *
156b45c9c31SVlad Serebrennikov SemaHLSL::mergeParamModifierAttr(Decl *D, const AttributeCommonInfo &AL,
157b45c9c31SVlad Serebrennikov                                  HLSLParamModifierAttr::Spelling Spelling) {
158b45c9c31SVlad Serebrennikov   // We can only merge an `in` attribute with an `out` attribute. All other
159b45c9c31SVlad Serebrennikov   // combinations of duplicated attributes are ill-formed.
160b45c9c31SVlad Serebrennikov   if (HLSLParamModifierAttr *PA = D->getAttr<HLSLParamModifierAttr>()) {
161b45c9c31SVlad Serebrennikov     if ((PA->isIn() && Spelling == HLSLParamModifierAttr::Keyword_out) ||
162b45c9c31SVlad Serebrennikov         (PA->isOut() && Spelling == HLSLParamModifierAttr::Keyword_in)) {
163b45c9c31SVlad Serebrennikov       D->dropAttr<HLSLParamModifierAttr>();
164b45c9c31SVlad Serebrennikov       SourceRange AdjustedRange = {PA->getLocation(), AL.getRange().getEnd()};
165b45c9c31SVlad Serebrennikov       return HLSLParamModifierAttr::Create(
166b45c9c31SVlad Serebrennikov           getASTContext(), /*MergedSpelling=*/true, AdjustedRange,
167b45c9c31SVlad Serebrennikov           HLSLParamModifierAttr::Keyword_inout);
168b45c9c31SVlad Serebrennikov     }
169b45c9c31SVlad Serebrennikov     Diag(AL.getLoc(), diag::err_hlsl_duplicate_parameter_modifier) << AL;
170b45c9c31SVlad Serebrennikov     Diag(PA->getLocation(), diag::note_conflicting_attribute);
171b45c9c31SVlad Serebrennikov     return nullptr;
172b45c9c31SVlad Serebrennikov   }
173b45c9c31SVlad Serebrennikov   return HLSLParamModifierAttr::Create(getASTContext(), AL);
174b45c9c31SVlad Serebrennikov }
175b45c9c31SVlad Serebrennikov 
176b45c9c31SVlad Serebrennikov void SemaHLSL::ActOnTopLevelFunction(FunctionDecl *FD) {
177b45c9c31SVlad Serebrennikov   auto &TargetInfo = getASTContext().getTargetInfo();
178b45c9c31SVlad Serebrennikov 
179b45c9c31SVlad Serebrennikov   if (FD->getName() != TargetInfo.getTargetOpts().HLSLEntry)
180b45c9c31SVlad Serebrennikov     return;
181b45c9c31SVlad Serebrennikov 
182b45c9c31SVlad Serebrennikov   StringRef Env = TargetInfo.getTriple().getEnvironmentName();
183b45c9c31SVlad Serebrennikov   HLSLShaderAttr::ShaderType ShaderType;
184b45c9c31SVlad Serebrennikov   if (HLSLShaderAttr::ConvertStrToShaderType(Env, ShaderType)) {
185b45c9c31SVlad Serebrennikov     if (const auto *Shader = FD->getAttr<HLSLShaderAttr>()) {
186b45c9c31SVlad Serebrennikov       // The entry point is already annotated - check that it matches the
187b45c9c31SVlad Serebrennikov       // triple.
188b45c9c31SVlad Serebrennikov       if (Shader->getType() != ShaderType) {
189b45c9c31SVlad Serebrennikov         Diag(Shader->getLocation(), diag::err_hlsl_entry_shader_attr_mismatch)
190b45c9c31SVlad Serebrennikov             << Shader;
191b45c9c31SVlad Serebrennikov         FD->setInvalidDecl();
192b45c9c31SVlad Serebrennikov       }
193b45c9c31SVlad Serebrennikov     } else {
194b45c9c31SVlad Serebrennikov       // Implicitly add the shader attribute if the entry function isn't
195b45c9c31SVlad Serebrennikov       // explicitly annotated.
196b45c9c31SVlad Serebrennikov       FD->addAttr(HLSLShaderAttr::CreateImplicit(getASTContext(), ShaderType,
197b45c9c31SVlad Serebrennikov                                                  FD->getBeginLoc()));
198b45c9c31SVlad Serebrennikov     }
199b45c9c31SVlad Serebrennikov   } else {
200b45c9c31SVlad Serebrennikov     switch (TargetInfo.getTriple().getEnvironment()) {
201b45c9c31SVlad Serebrennikov     case llvm::Triple::UnknownEnvironment:
202b45c9c31SVlad Serebrennikov     case llvm::Triple::Library:
203b45c9c31SVlad Serebrennikov       break;
204b45c9c31SVlad Serebrennikov     default:
205b45c9c31SVlad Serebrennikov       llvm_unreachable("Unhandled environment in triple");
206b45c9c31SVlad Serebrennikov     }
207b45c9c31SVlad Serebrennikov   }
208b45c9c31SVlad Serebrennikov }
209b45c9c31SVlad Serebrennikov 
210b45c9c31SVlad Serebrennikov void SemaHLSL::CheckEntryPoint(FunctionDecl *FD) {
211b45c9c31SVlad Serebrennikov   const auto *ShaderAttr = FD->getAttr<HLSLShaderAttr>();
212b45c9c31SVlad Serebrennikov   assert(ShaderAttr && "Entry point has no shader attribute");
213b45c9c31SVlad Serebrennikov   HLSLShaderAttr::ShaderType ST = ShaderAttr->getType();
214b45c9c31SVlad Serebrennikov 
215b45c9c31SVlad Serebrennikov   switch (ST) {
216b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Pixel:
217b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Vertex:
218b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Geometry:
219b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Hull:
220b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Domain:
221b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::RayGeneration:
222b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Intersection:
223b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::AnyHit:
224b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::ClosestHit:
225b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Miss:
226b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Callable:
227b45c9c31SVlad Serebrennikov     if (const auto *NT = FD->getAttr<HLSLNumThreadsAttr>()) {
228b45c9c31SVlad Serebrennikov       DiagnoseAttrStageMismatch(NT, ST,
229b45c9c31SVlad Serebrennikov                                 {HLSLShaderAttr::Compute,
230b45c9c31SVlad Serebrennikov                                  HLSLShaderAttr::Amplification,
231b45c9c31SVlad Serebrennikov                                  HLSLShaderAttr::Mesh});
232b45c9c31SVlad Serebrennikov       FD->setInvalidDecl();
233b45c9c31SVlad Serebrennikov     }
234b45c9c31SVlad Serebrennikov     break;
235b45c9c31SVlad Serebrennikov 
236b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Compute:
237b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Amplification:
238b45c9c31SVlad Serebrennikov   case HLSLShaderAttr::Mesh:
239b45c9c31SVlad Serebrennikov     if (!FD->hasAttr<HLSLNumThreadsAttr>()) {
240b45c9c31SVlad Serebrennikov       Diag(FD->getLocation(), diag::err_hlsl_missing_numthreads)
241b45c9c31SVlad Serebrennikov           << HLSLShaderAttr::ConvertShaderTypeToStr(ST);
242b45c9c31SVlad Serebrennikov       FD->setInvalidDecl();
243b45c9c31SVlad Serebrennikov     }
244b45c9c31SVlad Serebrennikov     break;
245b45c9c31SVlad Serebrennikov   }
246b45c9c31SVlad Serebrennikov 
247b45c9c31SVlad Serebrennikov   for (ParmVarDecl *Param : FD->parameters()) {
248b45c9c31SVlad Serebrennikov     if (const auto *AnnotationAttr = Param->getAttr<HLSLAnnotationAttr>()) {
249b45c9c31SVlad Serebrennikov       CheckSemanticAnnotation(FD, Param, AnnotationAttr);
250b45c9c31SVlad Serebrennikov     } else {
251b45c9c31SVlad Serebrennikov       // FIXME: Handle struct parameters where annotations are on struct fields.
252b45c9c31SVlad Serebrennikov       // See: https://github.com/llvm/llvm-project/issues/57875
253b45c9c31SVlad Serebrennikov       Diag(FD->getLocation(), diag::err_hlsl_missing_semantic_annotation);
254b45c9c31SVlad Serebrennikov       Diag(Param->getLocation(), diag::note_previous_decl) << Param;
255b45c9c31SVlad Serebrennikov       FD->setInvalidDecl();
256b45c9c31SVlad Serebrennikov     }
257b45c9c31SVlad Serebrennikov   }
258b45c9c31SVlad Serebrennikov   // FIXME: Verify return type semantic annotation.
259b45c9c31SVlad Serebrennikov }
260b45c9c31SVlad Serebrennikov 
261b45c9c31SVlad Serebrennikov void SemaHLSL::CheckSemanticAnnotation(
262b45c9c31SVlad Serebrennikov     FunctionDecl *EntryPoint, const Decl *Param,
263b45c9c31SVlad Serebrennikov     const HLSLAnnotationAttr *AnnotationAttr) {
264b45c9c31SVlad Serebrennikov   auto *ShaderAttr = EntryPoint->getAttr<HLSLShaderAttr>();
265b45c9c31SVlad Serebrennikov   assert(ShaderAttr && "Entry point has no shader attribute");
266b45c9c31SVlad Serebrennikov   HLSLShaderAttr::ShaderType ST = ShaderAttr->getType();
267b45c9c31SVlad Serebrennikov 
268b45c9c31SVlad Serebrennikov   switch (AnnotationAttr->getKind()) {
269b45c9c31SVlad Serebrennikov   case attr::HLSLSV_DispatchThreadID:
270b45c9c31SVlad Serebrennikov   case attr::HLSLSV_GroupIndex:
271b45c9c31SVlad Serebrennikov     if (ST == HLSLShaderAttr::Compute)
272b45c9c31SVlad Serebrennikov       return;
273b45c9c31SVlad Serebrennikov     DiagnoseAttrStageMismatch(AnnotationAttr, ST, {HLSLShaderAttr::Compute});
274b45c9c31SVlad Serebrennikov     break;
275b45c9c31SVlad Serebrennikov   default:
276b45c9c31SVlad Serebrennikov     llvm_unreachable("Unknown HLSLAnnotationAttr");
277b45c9c31SVlad Serebrennikov   }
278b45c9c31SVlad Serebrennikov }
279b45c9c31SVlad Serebrennikov 
280b45c9c31SVlad Serebrennikov void SemaHLSL::DiagnoseAttrStageMismatch(
281b45c9c31SVlad Serebrennikov     const Attr *A, HLSLShaderAttr::ShaderType Stage,
282b45c9c31SVlad Serebrennikov     std::initializer_list<HLSLShaderAttr::ShaderType> AllowedStages) {
283b45c9c31SVlad Serebrennikov   SmallVector<StringRef, 8> StageStrings;
284b45c9c31SVlad Serebrennikov   llvm::transform(AllowedStages, std::back_inserter(StageStrings),
285b45c9c31SVlad Serebrennikov                   [](HLSLShaderAttr::ShaderType ST) {
286b45c9c31SVlad Serebrennikov                     return StringRef(
287b45c9c31SVlad Serebrennikov                         HLSLShaderAttr::ConvertShaderTypeToStr(ST));
288b45c9c31SVlad Serebrennikov                   });
289b45c9c31SVlad Serebrennikov   Diag(A->getLoc(), diag::err_hlsl_attr_unsupported_in_stage)
290b45c9c31SVlad Serebrennikov       << A << HLSLShaderAttr::ConvertShaderTypeToStr(Stage)
291b45c9c31SVlad Serebrennikov       << (AllowedStages.size() != 1) << join(StageStrings, ", ");
292b45c9c31SVlad Serebrennikov }
293