blob: 510d610a6e27f2787ba2016166cb0b1139146196 [file] [log] [blame]
ethannicholasb3058bd2016-07-01 08:22:01 -07001/*
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 */
Mike Klein6ad99092016-10-26 10:35:22 -04007
ethannicholasb3058bd2016-07-01 08:22:01 -07008#include "SkSLCompiler.h"
9
10#include <fstream>
11#include <streambuf>
12
ethannicholas5961bc92016-10-12 06:39:56 -070013#include "ast/SkSLASTPrecision.h"
ethannicholas22f939e2016-10-13 13:25:34 -070014#include "SkSLCFGGenerator.h"
ethannicholasb3058bd2016-07-01 08:22:01 -070015#include "SkSLIRGenerator.h"
16#include "SkSLParser.h"
17#include "SkSLSPIRVCodeGenerator.h"
18#include "ir/SkSLExpression.h"
19#include "ir/SkSLIntLiteral.h"
ethannicholas5961bc92016-10-12 06:39:56 -070020#include "ir/SkSLModifiersDeclaration.h"
ethannicholasb3058bd2016-07-01 08:22:01 -070021#include "ir/SkSLSymbolTable.h"
ethannicholasddb37d62016-10-20 09:54:00 -070022#include "ir/SkSLUnresolvedFunction.h"
ethannicholas22f939e2016-10-13 13:25:34 -070023#include "ir/SkSLVarDeclarations.h"
ethannicholasb3058bd2016-07-01 08:22:01 -070024#include "SkMutex.h"
25
26#define STRINGIFY(x) #x
27
28// include the built-in shader symbols as static strings
29
ethannicholas5961bc92016-10-12 06:39:56 -070030static const char* SKSL_INCLUDE =
ethannicholasb3058bd2016-07-01 08:22:01 -070031#include "sksl.include"
32;
33
ethannicholas5961bc92016-10-12 06:39:56 -070034static const char* SKSL_VERT_INCLUDE =
ethannicholasb3058bd2016-07-01 08:22:01 -070035#include "sksl_vert.include"
36;
37
ethannicholas5961bc92016-10-12 06:39:56 -070038static const char* SKSL_FRAG_INCLUDE =
ethannicholasb3058bd2016-07-01 08:22:01 -070039#include "sksl_frag.include"
40;
41
42namespace SkSL {
43
Mike Klein6ad99092016-10-26 10:35:22 -040044Compiler::Compiler()
ethannicholasb3058bd2016-07-01 08:22:01 -070045: fErrorCount(0) {
46 auto types = std::shared_ptr<SymbolTable>(new SymbolTable(*this));
47 auto symbols = std::shared_ptr<SymbolTable>(new SymbolTable(types, *this));
ethannicholasd598f792016-07-25 10:08:54 -070048 fIRGenerator = new IRGenerator(&fContext, symbols, *this);
ethannicholasb3058bd2016-07-01 08:22:01 -070049 fTypes = types;
ethannicholasd598f792016-07-25 10:08:54 -070050 #define ADD_TYPE(t) types->addWithoutOwnership(fContext.f ## t ## _Type->fName, \
51 fContext.f ## t ## _Type.get())
ethannicholasb3058bd2016-07-01 08:22:01 -070052 ADD_TYPE(Void);
53 ADD_TYPE(Float);
54 ADD_TYPE(Vec2);
55 ADD_TYPE(Vec3);
56 ADD_TYPE(Vec4);
57 ADD_TYPE(Double);
58 ADD_TYPE(DVec2);
59 ADD_TYPE(DVec3);
60 ADD_TYPE(DVec4);
61 ADD_TYPE(Int);
62 ADD_TYPE(IVec2);
63 ADD_TYPE(IVec3);
64 ADD_TYPE(IVec4);
65 ADD_TYPE(UInt);
66 ADD_TYPE(UVec2);
67 ADD_TYPE(UVec3);
68 ADD_TYPE(UVec4);
69 ADD_TYPE(Bool);
70 ADD_TYPE(BVec2);
71 ADD_TYPE(BVec3);
72 ADD_TYPE(BVec4);
73 ADD_TYPE(Mat2x2);
ethannicholasf789b382016-08-03 12:43:36 -070074 types->addWithoutOwnership("mat2x2", fContext.fMat2x2_Type.get());
ethannicholasb3058bd2016-07-01 08:22:01 -070075 ADD_TYPE(Mat2x3);
76 ADD_TYPE(Mat2x4);
77 ADD_TYPE(Mat3x2);
78 ADD_TYPE(Mat3x3);
ethannicholasf789b382016-08-03 12:43:36 -070079 types->addWithoutOwnership("mat3x3", fContext.fMat3x3_Type.get());
ethannicholasb3058bd2016-07-01 08:22:01 -070080 ADD_TYPE(Mat3x4);
81 ADD_TYPE(Mat4x2);
82 ADD_TYPE(Mat4x3);
83 ADD_TYPE(Mat4x4);
ethannicholasf789b382016-08-03 12:43:36 -070084 types->addWithoutOwnership("mat4x4", fContext.fMat4x4_Type.get());
ethannicholasb3058bd2016-07-01 08:22:01 -070085 ADD_TYPE(GenType);
86 ADD_TYPE(GenDType);
87 ADD_TYPE(GenIType);
88 ADD_TYPE(GenUType);
89 ADD_TYPE(GenBType);
90 ADD_TYPE(Mat);
91 ADD_TYPE(Vec);
92 ADD_TYPE(GVec);
93 ADD_TYPE(GVec2);
94 ADD_TYPE(GVec3);
95 ADD_TYPE(GVec4);
96 ADD_TYPE(DVec);
97 ADD_TYPE(IVec);
98 ADD_TYPE(UVec);
99 ADD_TYPE(BVec);
100
101 ADD_TYPE(Sampler1D);
102 ADD_TYPE(Sampler2D);
103 ADD_TYPE(Sampler3D);
ethannicholas5961bc92016-10-12 06:39:56 -0700104 ADD_TYPE(SamplerExternalOES);
ethannicholasb3058bd2016-07-01 08:22:01 -0700105 ADD_TYPE(SamplerCube);
106 ADD_TYPE(Sampler2DRect);
107 ADD_TYPE(Sampler1DArray);
108 ADD_TYPE(Sampler2DArray);
109 ADD_TYPE(SamplerCubeArray);
110 ADD_TYPE(SamplerBuffer);
111 ADD_TYPE(Sampler2DMS);
112 ADD_TYPE(Sampler2DMSArray);
113
Brian Salomonbf7b6202016-11-11 16:08:03 -0500114 ADD_TYPE(ISampler2D);
115
Brian Salomon2a51de82016-11-16 12:06:01 -0500116 ADD_TYPE(Image2D);
117 ADD_TYPE(IImage2D);
118
ethannicholasb3058bd2016-07-01 08:22:01 -0700119 ADD_TYPE(GSampler1D);
120 ADD_TYPE(GSampler2D);
121 ADD_TYPE(GSampler3D);
122 ADD_TYPE(GSamplerCube);
123 ADD_TYPE(GSampler2DRect);
124 ADD_TYPE(GSampler1DArray);
125 ADD_TYPE(GSampler2DArray);
126 ADD_TYPE(GSamplerCubeArray);
127 ADD_TYPE(GSamplerBuffer);
128 ADD_TYPE(GSampler2DMS);
129 ADD_TYPE(GSampler2DMSArray);
130
131 ADD_TYPE(Sampler1DShadow);
132 ADD_TYPE(Sampler2DShadow);
133 ADD_TYPE(SamplerCubeShadow);
134 ADD_TYPE(Sampler2DRectShadow);
135 ADD_TYPE(Sampler1DArrayShadow);
136 ADD_TYPE(Sampler2DArrayShadow);
137 ADD_TYPE(SamplerCubeArrayShadow);
138 ADD_TYPE(GSampler2DArrayShadow);
139 ADD_TYPE(GSamplerCubeArrayShadow);
140
ethannicholas5961bc92016-10-12 06:39:56 -0700141 Modifiers::Flag ignored1;
142 std::vector<std::unique_ptr<ProgramElement>> ignored2;
143 this->internalConvertProgram(SKSL_INCLUDE, &ignored1, &ignored2);
ethannicholasddb37d62016-10-20 09:54:00 -0700144 fIRGenerator->fSymbolTable->markAllFunctionsBuiltin();
ethannicholasb3058bd2016-07-01 08:22:01 -0700145 ASSERT(!fErrorCount);
146}
147
148Compiler::~Compiler() {
149 delete fIRGenerator;
150}
151
ethannicholas22f939e2016-10-13 13:25:34 -0700152// add the definition created by assigning to the lvalue to the definition set
153void Compiler::addDefinition(const Expression* lvalue, const Expression* expr,
154 std::unordered_map<const Variable*, const Expression*>* definitions) {
155 switch (lvalue->fKind) {
156 case Expression::kVariableReference_Kind: {
157 const Variable& var = ((VariableReference*) lvalue)->fVariable;
158 if (var.fStorage == Variable::kLocal_Storage) {
159 (*definitions)[&var] = expr;
160 }
161 break;
162 }
163 case Expression::kSwizzle_Kind:
164 // We consider the variable written to as long as at least some of its components have
165 // been written to. This will lead to some false negatives (we won't catch it if you
166 // write to foo.x and then read foo.y), but being stricter could lead to false positives
Mike Klein6ad99092016-10-26 10:35:22 -0400167 // (we write to foo.x, and then pass foo to a function which happens to only read foo.x,
168 // but since we pass foo as a whole it is flagged as an error) unless we perform a much
ethannicholas22f939e2016-10-13 13:25:34 -0700169 // more complicated whole-program analysis. This is probably good enough.
Mike Klein6ad99092016-10-26 10:35:22 -0400170 this->addDefinition(((Swizzle*) lvalue)->fBase.get(),
171 fContext.fDefined_Expression.get(),
ethannicholas22f939e2016-10-13 13:25:34 -0700172 definitions);
173 break;
174 case Expression::kIndex_Kind:
175 // see comments in Swizzle
Mike Klein6ad99092016-10-26 10:35:22 -0400176 this->addDefinition(((IndexExpression*) lvalue)->fBase.get(),
177 fContext.fDefined_Expression.get(),
ethannicholas22f939e2016-10-13 13:25:34 -0700178 definitions);
179 break;
180 case Expression::kFieldAccess_Kind:
181 // see comments in Swizzle
Mike Klein6ad99092016-10-26 10:35:22 -0400182 this->addDefinition(((FieldAccess*) lvalue)->fBase.get(),
183 fContext.fDefined_Expression.get(),
ethannicholas22f939e2016-10-13 13:25:34 -0700184 definitions);
185 break;
186 default:
187 // not an lvalue, can't happen
188 ASSERT(false);
189 }
190}
191
192// add local variables defined by this node to the set
Mike Klein6ad99092016-10-26 10:35:22 -0400193void Compiler::addDefinitions(const BasicBlock::Node& node,
ethannicholas22f939e2016-10-13 13:25:34 -0700194 std::unordered_map<const Variable*, const Expression*>* definitions) {
195 switch (node.fKind) {
196 case BasicBlock::Node::kExpression_Kind: {
197 const Expression* expr = (Expression*) node.fNode;
198 if (expr->fKind == Expression::kBinary_Kind) {
199 const BinaryExpression* b = (BinaryExpression*) expr;
200 if (b->fOperator == Token::EQ) {
201 this->addDefinition(b->fLeft.get(), b->fRight.get(), definitions);
202 }
203 }
204 break;
205 }
206 case BasicBlock::Node::kStatement_Kind: {
207 const Statement* stmt = (Statement*) node.fNode;
208 if (stmt->fKind == Statement::kVarDeclarations_Kind) {
209 const VarDeclarationsStatement* vd = (VarDeclarationsStatement*) stmt;
210 for (const VarDeclaration& decl : vd->fDeclaration->fVars) {
211 if (decl.fValue) {
212 (*definitions)[decl.fVar] = decl.fValue.get();
213 }
214 }
215 }
216 break;
217 }
218 }
219}
220
221void Compiler::scanCFG(CFG* cfg, BlockId blockId, std::set<BlockId>* workList) {
222 BasicBlock& block = cfg->fBlocks[blockId];
223
224 // compute definitions after this block
225 std::unordered_map<const Variable*, const Expression*> after = block.fBefore;
226 for (const BasicBlock::Node& n : block.fNodes) {
227 this->addDefinitions(n, &after);
228 }
229
230 // propagate definitions to exits
231 for (BlockId exitId : block.fExits) {
232 BasicBlock& exit = cfg->fBlocks[exitId];
233 for (const auto& pair : after) {
234 const Expression* e1 = pair.second;
235 if (exit.fBefore.find(pair.first) == exit.fBefore.end()) {
236 exit.fBefore[pair.first] = e1;
237 } else {
238 const Expression* e2 = exit.fBefore[pair.first];
239 if (e1 != e2) {
240 // definition has changed, merge and add exit block to worklist
241 workList->insert(exitId);
242 if (!e1 || !e2) {
243 exit.fBefore[pair.first] = nullptr;
244 } else {
245 exit.fBefore[pair.first] = fContext.fDefined_Expression.get();
246 }
247 }
248 }
249 }
250 }
251}
252
253// returns a map which maps all local variables in the function to null, indicating that their value
254// is initially unknown
255static std::unordered_map<const Variable*, const Expression*> compute_start_state(const CFG& cfg) {
256 std::unordered_map<const Variable*, const Expression*> result;
Mike Klein6ad99092016-10-26 10:35:22 -0400257 for (const auto& block : cfg.fBlocks) {
258 for (const auto& node : block.fNodes) {
ethannicholas22f939e2016-10-13 13:25:34 -0700259 if (node.fKind == BasicBlock::Node::kStatement_Kind) {
260 const Statement* s = (Statement*) node.fNode;
261 if (s->fKind == Statement::kVarDeclarations_Kind) {
262 const VarDeclarationsStatement* vd = (const VarDeclarationsStatement*) s;
263 for (const VarDeclaration& decl : vd->fDeclaration->fVars) {
264 result[decl.fVar] = nullptr;
Mike Klein6ad99092016-10-26 10:35:22 -0400265 }
ethannicholas22f939e2016-10-13 13:25:34 -0700266 }
267 }
268 }
269 }
270 return result;
271}
272
273void Compiler::scanCFG(const FunctionDefinition& f) {
274 CFG cfg = CFGGenerator().getCFG(f);
275
276 // compute the data flow
277 cfg.fBlocks[cfg.fStart].fBefore = compute_start_state(cfg);
278 std::set<BlockId> workList;
279 for (BlockId i = 0; i < cfg.fBlocks.size(); i++) {
280 workList.insert(i);
281 }
282 while (workList.size()) {
283 BlockId next = *workList.begin();
284 workList.erase(workList.begin());
285 this->scanCFG(&cfg, next, &workList);
286 }
287
288 // check for unreachable code
289 for (size_t i = 0; i < cfg.fBlocks.size(); i++) {
Mike Klein6ad99092016-10-26 10:35:22 -0400290 if (i != cfg.fStart && !cfg.fBlocks[i].fEntrances.size() &&
ethannicholas22f939e2016-10-13 13:25:34 -0700291 cfg.fBlocks[i].fNodes.size()) {
292 this->error(cfg.fBlocks[i].fNodes[0].fNode->fPosition, "unreachable");
293 }
294 }
295 if (fErrorCount) {
296 return;
297 }
298
299 // check for undefined variables
300 for (const BasicBlock& b : cfg.fBlocks) {
301 std::unordered_map<const Variable*, const Expression*> definitions = b.fBefore;
302 for (const BasicBlock::Node& n : b.fNodes) {
303 if (n.fKind == BasicBlock::Node::kExpression_Kind) {
304 const Expression* expr = (const Expression*) n.fNode;
305 if (expr->fKind == Expression::kVariableReference_Kind) {
306 const Variable& var = ((VariableReference*) expr)->fVariable;
Mike Klein6ad99092016-10-26 10:35:22 -0400307 if (var.fStorage == Variable::kLocal_Storage &&
ethannicholas22f939e2016-10-13 13:25:34 -0700308 !definitions[&var]) {
309 this->error(expr->fPosition,
310 "'" + var.fName + "' has not been assigned");
Mike Klein6ad99092016-10-26 10:35:22 -0400311 }
ethannicholas22f939e2016-10-13 13:25:34 -0700312 }
313 }
314 this->addDefinitions(n, &definitions);
315 }
316 }
317
318 // check for missing return
319 if (f.fDeclaration.fReturnType != *fContext.fVoid_Type) {
320 if (cfg.fBlocks[cfg.fExit].fEntrances.size()) {
321 this->error(f.fPosition, "function can exit without returning a value");
322 }
323 }
324}
325
ethannicholasb3058bd2016-07-01 08:22:01 -0700326void Compiler::internalConvertProgram(std::string text,
ethannicholas5961bc92016-10-12 06:39:56 -0700327 Modifiers::Flag* defaultPrecision,
ethannicholasb3058bd2016-07-01 08:22:01 -0700328 std::vector<std::unique_ptr<ProgramElement>>* result) {
329 Parser parser(text, *fTypes, *this);
330 std::vector<std::unique_ptr<ASTDeclaration>> parsed = parser.file();
331 if (fErrorCount) {
332 return;
333 }
ethannicholas5961bc92016-10-12 06:39:56 -0700334 *defaultPrecision = Modifiers::kHighp_Flag;
ethannicholasb3058bd2016-07-01 08:22:01 -0700335 for (size_t i = 0; i < parsed.size(); i++) {
336 ASTDeclaration& decl = *parsed[i];
337 switch (decl.fKind) {
338 case ASTDeclaration::kVar_Kind: {
ethannicholas14fe8cc2016-09-07 13:37:16 -0700339 std::unique_ptr<VarDeclarations> s = fIRGenerator->convertVarDeclarations(
Mike Klein6ad99092016-10-26 10:35:22 -0400340 (ASTVarDeclarations&) decl,
ethannicholasb3058bd2016-07-01 08:22:01 -0700341 Variable::kGlobal_Storage);
342 if (s) {
343 result->push_back(std::move(s));
344 }
345 break;
346 }
347 case ASTDeclaration::kFunction_Kind: {
348 std::unique_ptr<FunctionDefinition> f = fIRGenerator->convertFunction(
349 (ASTFunction&) decl);
ethannicholas22f939e2016-10-13 13:25:34 -0700350 if (!fErrorCount && f) {
351 this->scanCFG(*f);
ethannicholasb3058bd2016-07-01 08:22:01 -0700352 result->push_back(std::move(f));
353 }
354 break;
355 }
ethannicholas5961bc92016-10-12 06:39:56 -0700356 case ASTDeclaration::kModifiers_Kind: {
357 std::unique_ptr<ModifiersDeclaration> f = fIRGenerator->convertModifiersDeclaration(
358 (ASTModifiersDeclaration&) decl);
359 if (f) {
360 result->push_back(std::move(f));
361 }
362 break;
363 }
ethannicholasb3058bd2016-07-01 08:22:01 -0700364 case ASTDeclaration::kInterfaceBlock_Kind: {
365 std::unique_ptr<InterfaceBlock> i = fIRGenerator->convertInterfaceBlock(
366 (ASTInterfaceBlock&) decl);
367 if (i) {
368 result->push_back(std::move(i));
369 }
370 break;
371 }
372 case ASTDeclaration::kExtension_Kind: {
373 std::unique_ptr<Extension> e = fIRGenerator->convertExtension((ASTExtension&) decl);
374 if (e) {
375 result->push_back(std::move(e));
376 }
377 break;
378 }
ethannicholas5961bc92016-10-12 06:39:56 -0700379 case ASTDeclaration::kPrecision_Kind: {
380 *defaultPrecision = ((ASTPrecision&) decl).fPrecision;
381 break;
382 }
ethannicholasb3058bd2016-07-01 08:22:01 -0700383 default:
384 ABORT("unsupported declaration: %s\n", decl.description().c_str());
385 }
386 }
387}
388
389std::unique_ptr<Program> Compiler::convertProgram(Program::Kind kind, std::string text) {
390 fErrorText = "";
391 fErrorCount = 0;
392 fIRGenerator->pushSymbolTable();
ethannicholasd598f792016-07-25 10:08:54 -0700393 std::vector<std::unique_ptr<ProgramElement>> elements;
ethannicholas5961bc92016-10-12 06:39:56 -0700394 Modifiers::Flag ignored;
ethannicholasb3058bd2016-07-01 08:22:01 -0700395 switch (kind) {
396 case Program::kVertex_Kind:
ethannicholas5961bc92016-10-12 06:39:56 -0700397 this->internalConvertProgram(SKSL_VERT_INCLUDE, &ignored, &elements);
ethannicholasb3058bd2016-07-01 08:22:01 -0700398 break;
399 case Program::kFragment_Kind:
ethannicholas5961bc92016-10-12 06:39:56 -0700400 this->internalConvertProgram(SKSL_FRAG_INCLUDE, &ignored, &elements);
ethannicholasb3058bd2016-07-01 08:22:01 -0700401 break;
402 }
ethannicholasddb37d62016-10-20 09:54:00 -0700403 fIRGenerator->fSymbolTable->markAllFunctionsBuiltin();
ethannicholas5961bc92016-10-12 06:39:56 -0700404 Modifiers::Flag defaultPrecision;
405 this->internalConvertProgram(text, &defaultPrecision, &elements);
Mike Klein6ad99092016-10-26 10:35:22 -0400406 auto result = std::unique_ptr<Program>(new Program(kind, defaultPrecision, std::move(elements),
ethannicholasddb37d62016-10-20 09:54:00 -0700407 fIRGenerator->fSymbolTable));
ethannicholasb3058bd2016-07-01 08:22:01 -0700408 fIRGenerator->popSymbolTable();
409 this->writeErrorCount();
ethannicholasd598f792016-07-25 10:08:54 -0700410 return result;
ethannicholasb3058bd2016-07-01 08:22:01 -0700411}
412
413void Compiler::error(Position position, std::string msg) {
414 fErrorCount++;
415 fErrorText += "error: " + position.description() + ": " + msg.c_str() + "\n";
416}
417
418std::string Compiler::errorText() {
419 std::string result = fErrorText;
420 return result;
421}
422
423void Compiler::writeErrorCount() {
424 if (fErrorCount) {
425 fErrorText += to_string(fErrorCount) + " error";
426 if (fErrorCount > 1) {
427 fErrorText += "s";
428 }
429 fErrorText += "\n";
430 }
431}
432
ethannicholasf789b382016-08-03 12:43:36 -0700433bool Compiler::toSPIRV(Program::Kind kind, const std::string& text, std::ostream& out) {
ethannicholasb3058bd2016-07-01 08:22:01 -0700434 auto program = this->convertProgram(kind, text);
435 if (fErrorCount == 0) {
ethannicholasd598f792016-07-25 10:08:54 -0700436 SkSL::SPIRVCodeGenerator cg(&fContext);
ethannicholasb3058bd2016-07-01 08:22:01 -0700437 cg.generateCode(*program.get(), out);
438 ASSERT(!out.rdstate());
439 }
440 return fErrorCount == 0;
441}
442
ethannicholasf789b382016-08-03 12:43:36 -0700443bool Compiler::toSPIRV(Program::Kind kind, const std::string& text, std::string* out) {
ethannicholasb3058bd2016-07-01 08:22:01 -0700444 std::stringstream buffer;
445 bool result = this->toSPIRV(kind, text, buffer);
446 if (result) {
447 *out = buffer.str();
448 }
ethannicholasf789b382016-08-03 12:43:36 -0700449 return result;
450}
451
Ethan Nicholas7ef4b742016-11-11 15:16:46 -0500452bool Compiler::toGLSL(Program::Kind kind, const std::string& text, const GrGLSLCaps& caps,
ethannicholasf789b382016-08-03 12:43:36 -0700453 std::ostream& out) {
454 auto program = this->convertProgram(kind, text);
455 if (fErrorCount == 0) {
Ethan Nicholas7ef4b742016-11-11 15:16:46 -0500456 SkSL::GLSLCodeGenerator cg(&fContext, &caps);
ethannicholasf789b382016-08-03 12:43:36 -0700457 cg.generateCode(*program.get(), out);
458 ASSERT(!out.rdstate());
459 }
ethannicholasb3058bd2016-07-01 08:22:01 -0700460 return fErrorCount == 0;
461}
462
Ethan Nicholas7ef4b742016-11-11 15:16:46 -0500463bool Compiler::toGLSL(Program::Kind kind, const std::string& text, const GrGLSLCaps& caps,
ethannicholasf789b382016-08-03 12:43:36 -0700464 std::string* out) {
465 std::stringstream buffer;
466 bool result = this->toGLSL(kind, text, caps, buffer);
467 if (result) {
468 *out = buffer.str();
469 }
470 return result;
471}
472
ethannicholasb3058bd2016-07-01 08:22:01 -0700473} // namespace