Line data Source code
1 : /*
2 : * Copyright 2016 Google Inc.
3 : *
4 : * Use of this source code is governed by a BSD-style license that can be
5 : * found in the LICENSE file.
6 : */
7 :
8 : #include "SkSLCompiler.h"
9 :
10 : #include "ast/SkSLASTPrecision.h"
11 : #include "SkSLCFGGenerator.h"
12 : #include "SkSLGLSLCodeGenerator.h"
13 : #include "SkSLIRGenerator.h"
14 : #include "SkSLParser.h"
15 : #include "SkSLSPIRVCodeGenerator.h"
16 : #include "ir/SkSLExpression.h"
17 : #include "ir/SkSLIntLiteral.h"
18 : #include "ir/SkSLModifiersDeclaration.h"
19 : #include "ir/SkSLSymbolTable.h"
20 : #include "ir/SkSLUnresolvedFunction.h"
21 : #include "ir/SkSLVarDeclarations.h"
22 :
23 : #ifdef SK_ENABLE_SPIRV_VALIDATION
24 : #include "spirv-tools/libspirv.hpp"
25 : #endif
26 :
27 : #define STRINGIFY(x) #x
28 :
29 : // include the built-in shader symbols as static strings
30 :
31 : static const char* SKSL_INCLUDE =
32 : #include "sksl.include"
33 : ;
34 :
35 : static const char* SKSL_VERT_INCLUDE =
36 : #include "sksl_vert.include"
37 : ;
38 :
39 : static const char* SKSL_FRAG_INCLUDE =
40 : #include "sksl_frag.include"
41 : ;
42 :
43 : static const char* SKSL_GEOM_INCLUDE =
44 : #include "sksl_geom.include"
45 : ;
46 :
47 : namespace SkSL {
48 :
49 0 : Compiler::Compiler()
50 0 : : fErrorCount(0) {
51 0 : auto types = std::shared_ptr<SymbolTable>(new SymbolTable(this));
52 0 : auto symbols = std::shared_ptr<SymbolTable>(new SymbolTable(types, this));
53 0 : fIRGenerator = new IRGenerator(&fContext, symbols, *this);
54 0 : fTypes = types;
55 : #define ADD_TYPE(t) types->addWithoutOwnership(fContext.f ## t ## _Type->fName, \
56 : fContext.f ## t ## _Type.get())
57 0 : ADD_TYPE(Void);
58 0 : ADD_TYPE(Float);
59 0 : ADD_TYPE(Vec2);
60 0 : ADD_TYPE(Vec3);
61 0 : ADD_TYPE(Vec4);
62 0 : ADD_TYPE(Double);
63 0 : ADD_TYPE(DVec2);
64 0 : ADD_TYPE(DVec3);
65 0 : ADD_TYPE(DVec4);
66 0 : ADD_TYPE(Int);
67 0 : ADD_TYPE(IVec2);
68 0 : ADD_TYPE(IVec3);
69 0 : ADD_TYPE(IVec4);
70 0 : ADD_TYPE(UInt);
71 0 : ADD_TYPE(UVec2);
72 0 : ADD_TYPE(UVec3);
73 0 : ADD_TYPE(UVec4);
74 0 : ADD_TYPE(Bool);
75 0 : ADD_TYPE(BVec2);
76 0 : ADD_TYPE(BVec3);
77 0 : ADD_TYPE(BVec4);
78 0 : ADD_TYPE(Mat2x2);
79 0 : types->addWithoutOwnership(String("mat2x2"), fContext.fMat2x2_Type.get());
80 0 : ADD_TYPE(Mat2x3);
81 0 : ADD_TYPE(Mat2x4);
82 0 : ADD_TYPE(Mat3x2);
83 0 : ADD_TYPE(Mat3x3);
84 0 : types->addWithoutOwnership(String("mat3x3"), fContext.fMat3x3_Type.get());
85 0 : ADD_TYPE(Mat3x4);
86 0 : ADD_TYPE(Mat4x2);
87 0 : ADD_TYPE(Mat4x3);
88 0 : ADD_TYPE(Mat4x4);
89 0 : types->addWithoutOwnership(String("mat4x4"), fContext.fMat4x4_Type.get());
90 0 : ADD_TYPE(GenType);
91 0 : ADD_TYPE(GenDType);
92 0 : ADD_TYPE(GenIType);
93 0 : ADD_TYPE(GenUType);
94 0 : ADD_TYPE(GenBType);
95 0 : ADD_TYPE(Mat);
96 0 : ADD_TYPE(Vec);
97 0 : ADD_TYPE(GVec);
98 0 : ADD_TYPE(GVec2);
99 0 : ADD_TYPE(GVec3);
100 0 : ADD_TYPE(GVec4);
101 0 : ADD_TYPE(DVec);
102 0 : ADD_TYPE(IVec);
103 0 : ADD_TYPE(UVec);
104 0 : ADD_TYPE(BVec);
105 :
106 0 : ADD_TYPE(Sampler1D);
107 0 : ADD_TYPE(Sampler2D);
108 0 : ADD_TYPE(Sampler3D);
109 0 : ADD_TYPE(SamplerExternalOES);
110 0 : ADD_TYPE(SamplerCube);
111 0 : ADD_TYPE(Sampler2DRect);
112 0 : ADD_TYPE(Sampler1DArray);
113 0 : ADD_TYPE(Sampler2DArray);
114 0 : ADD_TYPE(SamplerCubeArray);
115 0 : ADD_TYPE(SamplerBuffer);
116 0 : ADD_TYPE(Sampler2DMS);
117 0 : ADD_TYPE(Sampler2DMSArray);
118 :
119 0 : ADD_TYPE(ISampler2D);
120 :
121 0 : ADD_TYPE(Image2D);
122 0 : ADD_TYPE(IImage2D);
123 :
124 0 : ADD_TYPE(SubpassInput);
125 0 : ADD_TYPE(SubpassInputMS);
126 :
127 0 : ADD_TYPE(GSampler1D);
128 0 : ADD_TYPE(GSampler2D);
129 0 : ADD_TYPE(GSampler3D);
130 0 : ADD_TYPE(GSamplerCube);
131 0 : ADD_TYPE(GSampler2DRect);
132 0 : ADD_TYPE(GSampler1DArray);
133 0 : ADD_TYPE(GSampler2DArray);
134 0 : ADD_TYPE(GSamplerCubeArray);
135 0 : ADD_TYPE(GSamplerBuffer);
136 0 : ADD_TYPE(GSampler2DMS);
137 0 : ADD_TYPE(GSampler2DMSArray);
138 :
139 0 : ADD_TYPE(Sampler1DShadow);
140 0 : ADD_TYPE(Sampler2DShadow);
141 0 : ADD_TYPE(SamplerCubeShadow);
142 0 : ADD_TYPE(Sampler2DRectShadow);
143 0 : ADD_TYPE(Sampler1DArrayShadow);
144 0 : ADD_TYPE(Sampler2DArrayShadow);
145 0 : ADD_TYPE(SamplerCubeArrayShadow);
146 0 : ADD_TYPE(GSampler2DArrayShadow);
147 0 : ADD_TYPE(GSamplerCubeArrayShadow);
148 :
149 0 : String skCapsName("sk_Caps");
150 0 : Variable* skCaps = new Variable(Position(), Modifiers(), skCapsName,
151 0 : *fContext.fSkCaps_Type, Variable::kGlobal_Storage);
152 0 : fIRGenerator->fSymbolTable->add(skCapsName, std::unique_ptr<Symbol>(skCaps));
153 :
154 : Modifiers::Flag ignored1;
155 0 : std::vector<std::unique_ptr<ProgramElement>> ignored2;
156 0 : this->internalConvertProgram(String(SKSL_INCLUDE), &ignored1, &ignored2);
157 0 : fIRGenerator->fSymbolTable->markAllFunctionsBuiltin();
158 0 : ASSERT(!fErrorCount);
159 0 : }
160 :
161 0 : Compiler::~Compiler() {
162 0 : delete fIRGenerator;
163 0 : }
164 :
165 : // add the definition created by assigning to the lvalue to the definition set
166 0 : void Compiler::addDefinition(const Expression* lvalue, std::unique_ptr<Expression>* expr,
167 : DefinitionMap* definitions) {
168 0 : switch (lvalue->fKind) {
169 : case Expression::kVariableReference_Kind: {
170 0 : const Variable& var = ((VariableReference*) lvalue)->fVariable;
171 0 : if (var.fStorage == Variable::kLocal_Storage) {
172 0 : (*definitions)[&var] = expr;
173 : }
174 0 : break;
175 : }
176 : case Expression::kSwizzle_Kind:
177 : // We consider the variable written to as long as at least some of its components have
178 : // been written to. This will lead to some false negatives (we won't catch it if you
179 : // write to foo.x and then read foo.y), but being stricter could lead to false positives
180 : // (we write to foo.x, and then pass foo to a function which happens to only read foo.x,
181 : // but since we pass foo as a whole it is flagged as an error) unless we perform a much
182 : // more complicated whole-program analysis. This is probably good enough.
183 0 : this->addDefinition(((Swizzle*) lvalue)->fBase.get(),
184 0 : (std::unique_ptr<Expression>*) &fContext.fDefined_Expression,
185 0 : definitions);
186 0 : break;
187 : case Expression::kIndex_Kind:
188 : // see comments in Swizzle
189 0 : this->addDefinition(((IndexExpression*) lvalue)->fBase.get(),
190 0 : (std::unique_ptr<Expression>*) &fContext.fDefined_Expression,
191 0 : definitions);
192 0 : break;
193 : case Expression::kFieldAccess_Kind:
194 : // see comments in Swizzle
195 0 : this->addDefinition(((FieldAccess*) lvalue)->fBase.get(),
196 0 : (std::unique_ptr<Expression>*) &fContext.fDefined_Expression,
197 0 : definitions);
198 0 : break;
199 : default:
200 : // not an lvalue, can't happen
201 0 : ASSERT(false);
202 : }
203 0 : }
204 :
205 : // add local variables defined by this node to the set
206 0 : void Compiler::addDefinitions(const BasicBlock::Node& node,
207 : DefinitionMap* definitions) {
208 0 : switch (node.fKind) {
209 : case BasicBlock::Node::kExpression_Kind: {
210 0 : ASSERT(node.fExpression);
211 0 : const Expression* expr = (Expression*) node.fExpression->get();
212 0 : switch (expr->fKind) {
213 : case Expression::kBinary_Kind: {
214 0 : BinaryExpression* b = (BinaryExpression*) expr;
215 0 : if (b->fOperator == Token::EQ) {
216 0 : this->addDefinition(b->fLeft.get(), &b->fRight, definitions);
217 0 : } else if (Token::IsAssignment(b->fOperator)) {
218 : this->addDefinition(
219 0 : b->fLeft.get(),
220 0 : (std::unique_ptr<Expression>*) &fContext.fDefined_Expression,
221 0 : definitions);
222 :
223 : }
224 0 : break;
225 : }
226 : case Expression::kPrefix_Kind: {
227 0 : const PrefixExpression* p = (PrefixExpression*) expr;
228 0 : if (p->fOperator == Token::MINUSMINUS || p->fOperator == Token::PLUSPLUS) {
229 : this->addDefinition(
230 0 : p->fOperand.get(),
231 0 : (std::unique_ptr<Expression>*) &fContext.fDefined_Expression,
232 0 : definitions);
233 : }
234 0 : break;
235 : }
236 : case Expression::kPostfix_Kind: {
237 0 : const PostfixExpression* p = (PostfixExpression*) expr;
238 0 : if (p->fOperator == Token::MINUSMINUS || p->fOperator == Token::PLUSPLUS) {
239 : this->addDefinition(
240 0 : p->fOperand.get(),
241 0 : (std::unique_ptr<Expression>*) &fContext.fDefined_Expression,
242 0 : definitions);
243 :
244 : }
245 0 : break;
246 : }
247 : default:
248 0 : break;
249 : }
250 0 : break;
251 : }
252 : case BasicBlock::Node::kStatement_Kind: {
253 0 : const Statement* stmt = (Statement*) node.fStatement;
254 0 : if (stmt->fKind == Statement::kVarDeclarations_Kind) {
255 0 : VarDeclarationsStatement* vd = (VarDeclarationsStatement*) stmt;
256 0 : for (VarDeclaration& decl : vd->fDeclaration->fVars) {
257 0 : if (decl.fValue) {
258 0 : (*definitions)[decl.fVar] = &decl.fValue;
259 : }
260 : }
261 : }
262 0 : break;
263 : }
264 : }
265 0 : }
266 :
267 0 : void Compiler::scanCFG(CFG* cfg, BlockId blockId, std::set<BlockId>* workList) {
268 0 : BasicBlock& block = cfg->fBlocks[blockId];
269 :
270 : // compute definitions after this block
271 0 : DefinitionMap after = block.fBefore;
272 0 : for (const BasicBlock::Node& n : block.fNodes) {
273 0 : this->addDefinitions(n, &after);
274 : }
275 :
276 : // propagate definitions to exits
277 0 : for (BlockId exitId : block.fExits) {
278 0 : BasicBlock& exit = cfg->fBlocks[exitId];
279 0 : for (const auto& pair : after) {
280 0 : std::unique_ptr<Expression>* e1 = pair.second;
281 0 : auto found = exit.fBefore.find(pair.first);
282 0 : if (found == exit.fBefore.end()) {
283 : // exit has no definition for it, just copy it
284 0 : workList->insert(exitId);
285 0 : exit.fBefore[pair.first] = e1;
286 : } else {
287 : // exit has a (possibly different) value already defined
288 0 : std::unique_ptr<Expression>* e2 = exit.fBefore[pair.first];
289 0 : if (e1 != e2) {
290 : // definition has changed, merge and add exit block to worklist
291 0 : workList->insert(exitId);
292 0 : if (e1 && e2) {
293 0 : exit.fBefore[pair.first] =
294 0 : (std::unique_ptr<Expression>*) &fContext.fDefined_Expression;
295 : } else {
296 0 : exit.fBefore[pair.first] = nullptr;
297 : }
298 : }
299 : }
300 : }
301 : }
302 0 : }
303 :
304 : // returns a map which maps all local variables in the function to null, indicating that their value
305 : // is initially unknown
306 0 : static DefinitionMap compute_start_state(const CFG& cfg) {
307 0 : DefinitionMap result;
308 0 : for (const auto& block : cfg.fBlocks) {
309 0 : for (const auto& node : block.fNodes) {
310 0 : if (node.fKind == BasicBlock::Node::kStatement_Kind) {
311 0 : ASSERT(node.fStatement);
312 0 : const Statement* s = node.fStatement;
313 0 : if (s->fKind == Statement::kVarDeclarations_Kind) {
314 0 : const VarDeclarationsStatement* vd = (const VarDeclarationsStatement*) s;
315 0 : for (const VarDeclaration& decl : vd->fDeclaration->fVars) {
316 0 : result[decl.fVar] = nullptr;
317 : }
318 : }
319 : }
320 : }
321 : }
322 0 : return result;
323 : }
324 :
325 0 : void Compiler::scanCFG(const FunctionDefinition& f) {
326 0 : CFG cfg = CFGGenerator().getCFG(f);
327 :
328 : // compute the data flow
329 0 : cfg.fBlocks[cfg.fStart].fBefore = compute_start_state(cfg);
330 0 : std::set<BlockId> workList;
331 0 : for (BlockId i = 0; i < cfg.fBlocks.size(); i++) {
332 0 : workList.insert(i);
333 : }
334 0 : while (workList.size()) {
335 0 : BlockId next = *workList.begin();
336 0 : workList.erase(workList.begin());
337 0 : this->scanCFG(&cfg, next, &workList);
338 : }
339 :
340 : // check for unreachable code
341 0 : for (size_t i = 0; i < cfg.fBlocks.size(); i++) {
342 0 : if (i != cfg.fStart && !cfg.fBlocks[i].fEntrances.size() &&
343 0 : cfg.fBlocks[i].fNodes.size()) {
344 0 : Position p;
345 0 : switch (cfg.fBlocks[i].fNodes[0].fKind) {
346 : case BasicBlock::Node::kStatement_Kind:
347 0 : p = cfg.fBlocks[i].fNodes[0].fStatement->fPosition;
348 0 : break;
349 : case BasicBlock::Node::kExpression_Kind:
350 0 : p = (*cfg.fBlocks[i].fNodes[0].fExpression)->fPosition;
351 0 : break;
352 : }
353 0 : this->error(p, String("unreachable"));
354 : }
355 : }
356 0 : if (fErrorCount) {
357 0 : return;
358 : }
359 :
360 : // check for undefined variables, perform constant propagation
361 0 : for (BasicBlock& b : cfg.fBlocks) {
362 0 : DefinitionMap definitions = b.fBefore;
363 0 : for (BasicBlock::Node& n : b.fNodes) {
364 0 : if (n.fKind == BasicBlock::Node::kExpression_Kind) {
365 0 : ASSERT(n.fExpression);
366 0 : Expression* expr = n.fExpression->get();
367 0 : if (n.fConstantPropagation) {
368 0 : std::unique_ptr<Expression> optimized = expr->constantPropagate(*fIRGenerator,
369 0 : definitions);
370 0 : if (optimized) {
371 0 : n.fExpression->reset(optimized.release());
372 0 : expr = n.fExpression->get();
373 : }
374 : }
375 0 : if (expr->fKind == Expression::kVariableReference_Kind) {
376 0 : const Variable& var = ((VariableReference*) expr)->fVariable;
377 0 : if (var.fStorage == Variable::kLocal_Storage &&
378 0 : !definitions[&var]) {
379 : this->error(expr->fPosition,
380 0 : "'" + var.fName + "' has not been assigned");
381 : }
382 : }
383 : }
384 0 : this->addDefinitions(n, &definitions);
385 : }
386 : }
387 :
388 : // check for missing return
389 0 : if (f.fDeclaration.fReturnType != *fContext.fVoid_Type) {
390 0 : if (cfg.fBlocks[cfg.fExit].fEntrances.size()) {
391 0 : this->error(f.fPosition, String("function can exit without returning a value"));
392 : }
393 : }
394 : }
395 :
396 0 : void Compiler::internalConvertProgram(String text,
397 : Modifiers::Flag* defaultPrecision,
398 : std::vector<std::unique_ptr<ProgramElement>>* result) {
399 0 : Parser parser(text, *fTypes, *this);
400 0 : std::vector<std::unique_ptr<ASTDeclaration>> parsed = parser.file();
401 0 : if (fErrorCount) {
402 0 : return;
403 : }
404 0 : *defaultPrecision = Modifiers::kHighp_Flag;
405 0 : for (size_t i = 0; i < parsed.size(); i++) {
406 0 : ASTDeclaration& decl = *parsed[i];
407 0 : switch (decl.fKind) {
408 : case ASTDeclaration::kVar_Kind: {
409 0 : std::unique_ptr<VarDeclarations> s = fIRGenerator->convertVarDeclarations(
410 : (ASTVarDeclarations&) decl,
411 0 : Variable::kGlobal_Storage);
412 0 : if (s) {
413 0 : result->push_back(std::move(s));
414 : }
415 0 : break;
416 : }
417 : case ASTDeclaration::kFunction_Kind: {
418 0 : std::unique_ptr<FunctionDefinition> f = fIRGenerator->convertFunction(
419 0 : (ASTFunction&) decl);
420 0 : if (!fErrorCount && f) {
421 0 : this->scanCFG(*f);
422 0 : result->push_back(std::move(f));
423 : }
424 0 : break;
425 : }
426 : case ASTDeclaration::kModifiers_Kind: {
427 0 : std::unique_ptr<ModifiersDeclaration> f = fIRGenerator->convertModifiersDeclaration(
428 0 : (ASTModifiersDeclaration&) decl);
429 0 : if (f) {
430 0 : result->push_back(std::move(f));
431 : }
432 0 : break;
433 : }
434 : case ASTDeclaration::kInterfaceBlock_Kind: {
435 0 : std::unique_ptr<InterfaceBlock> i = fIRGenerator->convertInterfaceBlock(
436 0 : (ASTInterfaceBlock&) decl);
437 0 : if (i) {
438 0 : result->push_back(std::move(i));
439 : }
440 0 : break;
441 : }
442 : case ASTDeclaration::kExtension_Kind: {
443 0 : std::unique_ptr<Extension> e = fIRGenerator->convertExtension((ASTExtension&) decl);
444 0 : if (e) {
445 0 : result->push_back(std::move(e));
446 : }
447 0 : break;
448 : }
449 : case ASTDeclaration::kPrecision_Kind: {
450 0 : *defaultPrecision = ((ASTPrecision&) decl).fPrecision;
451 0 : break;
452 : }
453 : default:
454 0 : ABORT("unsupported declaration: %s\n", decl.description().c_str());
455 : }
456 : }
457 : }
458 :
459 0 : std::unique_ptr<Program> Compiler::convertProgram(Program::Kind kind, String text,
460 : const Program::Settings& settings) {
461 0 : fErrorText = "";
462 0 : fErrorCount = 0;
463 0 : fIRGenerator->start(&settings);
464 0 : std::vector<std::unique_ptr<ProgramElement>> elements;
465 : Modifiers::Flag ignored;
466 0 : switch (kind) {
467 : case Program::kVertex_Kind:
468 0 : this->internalConvertProgram(String(SKSL_VERT_INCLUDE), &ignored, &elements);
469 0 : break;
470 : case Program::kFragment_Kind:
471 0 : this->internalConvertProgram(String(SKSL_FRAG_INCLUDE), &ignored, &elements);
472 0 : break;
473 : case Program::kGeometry_Kind:
474 0 : this->internalConvertProgram(String(SKSL_GEOM_INCLUDE), &ignored, &elements);
475 0 : break;
476 : }
477 0 : fIRGenerator->fSymbolTable->markAllFunctionsBuiltin();
478 : Modifiers::Flag defaultPrecision;
479 0 : this->internalConvertProgram(text, &defaultPrecision, &elements);
480 : auto result = std::unique_ptr<Program>(new Program(kind, settings, defaultPrecision, &fContext,
481 0 : std::move(elements),
482 0 : fIRGenerator->fSymbolTable,
483 0 : fIRGenerator->fInputs));
484 0 : fIRGenerator->finish();
485 0 : this->writeErrorCount();
486 0 : if (fErrorCount) {
487 0 : return nullptr;
488 : }
489 0 : return result;
490 : }
491 :
492 0 : bool Compiler::toSPIRV(const Program& program, OutputStream& out) {
493 : #ifdef SK_ENABLE_SPIRV_VALIDATION
494 : StringStream buffer;
495 : SPIRVCodeGenerator cg(&fContext, &program, this, &buffer);
496 : bool result = cg.generateCode();
497 : if (result) {
498 : spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_0);
499 : ASSERT(0 == buffer.size() % 4);
500 : auto dumpmsg = [](spv_message_level_t, const char*, const spv_position_t&, const char* m) {
501 : SkDebugf("SPIR-V validation error: %s\n", m);
502 : };
503 : tools.SetMessageConsumer(dumpmsg);
504 : // Verify that the SPIR-V we produced is valid. If this assert fails, check the logs prior
505 : // to the failure to see the validation errors.
506 : ASSERT_RESULT(tools.Validate((const uint32_t*) buffer.data(), buffer.size() / 4));
507 : out.write(buffer.data(), buffer.size());
508 : }
509 : #else
510 0 : SPIRVCodeGenerator cg(&fContext, &program, this, &out);
511 0 : bool result = cg.generateCode();
512 : #endif
513 0 : this->writeErrorCount();
514 0 : return result;
515 : }
516 :
517 0 : bool Compiler::toSPIRV(const Program& program, String* out) {
518 0 : StringStream buffer;
519 0 : bool result = this->toSPIRV(program, buffer);
520 0 : if (result) {
521 0 : *out = String(buffer.data(), buffer.size());
522 : }
523 0 : return result;
524 : }
525 :
526 0 : bool Compiler::toGLSL(const Program& program, OutputStream& out) {
527 0 : GLSLCodeGenerator cg(&fContext, &program, this, &out);
528 0 : bool result = cg.generateCode();
529 0 : this->writeErrorCount();
530 0 : return result;
531 : }
532 :
533 0 : bool Compiler::toGLSL(const Program& program, String* out) {
534 0 : StringStream buffer;
535 0 : bool result = this->toGLSL(program, buffer);
536 0 : if (result) {
537 0 : *out = String(buffer.data(), buffer.size());
538 : }
539 0 : return result;
540 : }
541 :
542 :
543 0 : void Compiler::error(Position position, String msg) {
544 0 : fErrorCount++;
545 0 : fErrorText += "error: " + position.description() + ": " + msg.c_str() + "\n";
546 0 : }
547 :
548 0 : String Compiler::errorText() {
549 0 : String result = fErrorText;
550 0 : return result;
551 : }
552 :
553 0 : void Compiler::writeErrorCount() {
554 0 : if (fErrorCount) {
555 0 : fErrorText += to_string(fErrorCount) + " error";
556 0 : if (fErrorCount > 1) {
557 0 : fErrorText += "s";
558 : }
559 0 : fErrorText += "\n";
560 : }
561 0 : }
562 :
563 : } // namespace
|