blob: 41aaceda5cc4011b050c41aee3226076401aafa4 [file] [log] [blame]
Olli Etuaho5d91dda2015-06-18 15:47:46 +03001//
2// Copyright (c) 2002-2015 The ANGLE Project Authors. All rights reserved.
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5//
6// RemoveDynamicIndexing is an AST traverser to remove dynamic indexing of vectors and matrices,
7// replacing them with calls to functions that choose which component to return or write.
8//
9
10#include "compiler/translator/RemoveDynamicIndexing.h"
11
12#include "compiler/translator/InfoSink.h"
13#include "compiler/translator/IntermNode.h"
Jamie Madill666f65a2016-08-26 01:34:37 +000014#include "compiler/translator/IntermNodePatternMatcher.h"
Olli Etuaho5d91dda2015-06-18 15:47:46 +030015#include "compiler/translator/SymbolTable.h"
16
Jamie Madill45bcc782016-11-07 13:58:48 -050017namespace sh
18{
19
Olli Etuaho5d91dda2015-06-18 15:47:46 +030020namespace
21{
22
23TName GetIndexFunctionName(const TType &type, bool write)
24{
25 TInfoSinkBase nameSink;
26 nameSink << "dyn_index_";
27 if (write)
28 {
29 nameSink << "write_";
30 }
31 if (type.isMatrix())
32 {
33 nameSink << "mat" << type.getCols() << "x" << type.getRows();
34 }
35 else
36 {
37 switch (type.getBasicType())
38 {
39 case EbtInt:
40 nameSink << "ivec";
41 break;
42 case EbtBool:
43 nameSink << "bvec";
44 break;
45 case EbtUInt:
46 nameSink << "uvec";
47 break;
48 case EbtFloat:
49 nameSink << "vec";
50 break;
51 default:
52 UNREACHABLE();
53 }
54 nameSink << type.getNominalSize();
55 }
56 TString nameString = TFunction::mangleName(nameSink.c_str());
57 TName name(nameString);
58 name.setInternal(true);
59 return name;
60}
61
62TIntermSymbol *CreateBaseSymbol(const TType &type, TQualifier qualifier)
63{
64 TIntermSymbol *symbol = new TIntermSymbol(0, "base", type);
65 symbol->setInternal(true);
66 symbol->getTypePointer()->setQualifier(qualifier);
67 return symbol;
68}
69
70TIntermSymbol *CreateIndexSymbol()
71{
72 TIntermSymbol *symbol = new TIntermSymbol(0, "index", TType(EbtInt, EbpHigh));
73 symbol->setInternal(true);
74 symbol->getTypePointer()->setQualifier(EvqIn);
75 return symbol;
76}
77
78TIntermSymbol *CreateValueSymbol(const TType &type)
79{
80 TIntermSymbol *symbol = new TIntermSymbol(0, "value", type);
81 symbol->setInternal(true);
82 symbol->getTypePointer()->setQualifier(EvqIn);
83 return symbol;
84}
85
86TIntermConstantUnion *CreateIntConstantNode(int i)
87{
88 TConstantUnion *constant = new TConstantUnion();
89 constant->setIConst(i);
90 return new TIntermConstantUnion(constant, TType(EbtInt, EbpHigh));
91}
92
93TIntermBinary *CreateIndexDirectBaseSymbolNode(const TType &indexedType,
94 const TType &fieldType,
95 const int index,
96 TQualifier baseQualifier)
97{
Olli Etuaho5d91dda2015-06-18 15:47:46 +030098 TIntermSymbol *baseSymbol = CreateBaseSymbol(indexedType, baseQualifier);
Olli Etuaho3272a6d2016-08-29 17:54:50 +030099 TIntermBinary *indexNode =
100 new TIntermBinary(EOpIndexDirect, baseSymbol, TIntermTyped::CreateIndexNode(index));
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300101 return indexNode;
102}
103
104TIntermBinary *CreateAssignValueSymbolNode(TIntermTyped *targetNode, const TType &assignedValueType)
105{
Olli Etuaho3272a6d2016-08-29 17:54:50 +0300106 return new TIntermBinary(EOpAssign, targetNode, CreateValueSymbol(assignedValueType));
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300107}
108
109TIntermTyped *EnsureSignedInt(TIntermTyped *node)
110{
111 if (node->getBasicType() == EbtInt)
112 return node;
113
114 TIntermAggregate *convertedNode = new TIntermAggregate(EOpConstructInt);
115 convertedNode->setType(TType(EbtInt));
116 convertedNode->getSequence()->push_back(node);
117 convertedNode->setPrecisionFromChildren();
118 return convertedNode;
119}
120
121TType GetFieldType(const TType &indexedType)
122{
123 if (indexedType.isMatrix())
124 {
125 TType fieldType = TType(indexedType.getBasicType(), indexedType.getPrecision());
126 fieldType.setPrimarySize(static_cast<unsigned char>(indexedType.getRows()));
127 return fieldType;
128 }
129 else
130 {
131 return TType(indexedType.getBasicType(), indexedType.getPrecision());
132 }
133}
134
135// Generate a read or write function for one field in a vector/matrix.
136// Out-of-range indices are clamped. This is consistent with how ANGLE handles out-of-range
137// indices in other places.
138// Note that indices can be either int or uint. We create only int versions of the functions,
139// and convert uint indices to int at the call site.
140// read function example:
141// float dyn_index_vec2(in vec2 base, in int index)
142// {
143// switch(index)
144// {
145// case (0):
146// return base[0];
147// case (1):
148// return base[1];
149// default:
150// break;
151// }
152// if (index < 0)
153// return base[0];
154// return base[1];
155// }
156// write function example:
157// void dyn_index_write_vec2(inout vec2 base, in int index, in float value)
158// {
159// switch(index)
160// {
161// case (0):
162// base[0] = value;
163// return;
164// case (1):
165// base[1] = value;
166// return;
167// default:
168// break;
169// }
170// if (index < 0)
171// {
172// base[0] = value;
173// return;
174// }
175// base[1] = value;
176// }
177// Note that else is not used in above functions to avoid the RewriteElseBlocks transformation.
Olli Etuaho336b1472016-10-05 16:37:55 +0100178TIntermFunctionDefinition *GetIndexFunctionDefinition(TType type, bool write)
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300179{
180 ASSERT(!type.isArray());
181 // Conservatively use highp here, even if the indexed type is not highp. That way the code can't
182 // end up using mediump version of an indexing function for a highp value, if both mediump and
183 // highp values are being indexed in the shader. For HLSL precision doesn't matter, but in
184 // principle this code could be used with multiple backends.
185 type.setPrecision(EbpHigh);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300186
187 TType fieldType = GetFieldType(type);
Jamie Madilld7b1ab52016-12-12 14:42:19 -0500188 int numCases = 0;
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300189 if (type.isMatrix())
190 {
191 numCases = type.getCols();
192 }
193 else
194 {
195 numCases = type.getNominalSize();
196 }
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300197
Olli Etuaho8ad9e752017-01-16 19:55:20 +0000198 TIntermFunctionPrototype *prototypeNode = nullptr;
199 if (write)
200 {
201 prototypeNode = new TIntermFunctionPrototype(TType(EbtVoid));
202 }
203 else
204 {
205 prototypeNode = new TIntermFunctionPrototype(fieldType);
206 }
207 prototypeNode->getFunctionSymbolInfo()->setNameObj(GetIndexFunctionName(type, write));
208
Jamie Madilld7b1ab52016-12-12 14:42:19 -0500209 TQualifier baseQualifier = EvqInOut;
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300210 if (!write)
211 baseQualifier = EvqIn;
212 TIntermSymbol *baseParam = CreateBaseSymbol(type, baseQualifier);
Olli Etuaho8ad9e752017-01-16 19:55:20 +0000213 prototypeNode->getSequence()->push_back(baseParam);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300214 TIntermSymbol *indexParam = CreateIndexSymbol();
Olli Etuaho8ad9e752017-01-16 19:55:20 +0000215 prototypeNode->getSequence()->push_back(indexParam);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300216 if (write)
217 {
218 TIntermSymbol *valueParam = CreateValueSymbol(fieldType);
Olli Etuaho8ad9e752017-01-16 19:55:20 +0000219 prototypeNode->getSequence()->push_back(valueParam);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300220 }
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300221
Olli Etuaho6d40bbd2016-09-30 13:49:38 +0100222 TIntermBlock *statementList = new TIntermBlock();
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300223 for (int i = 0; i < numCases; ++i)
224 {
225 TIntermCase *caseNode = new TIntermCase(CreateIntConstantNode(i));
226 statementList->getSequence()->push_back(caseNode);
227
228 TIntermBinary *indexNode =
229 CreateIndexDirectBaseSymbolNode(type, fieldType, i, baseQualifier);
230 if (write)
231 {
232 TIntermBinary *assignNode = CreateAssignValueSymbolNode(indexNode, fieldType);
233 statementList->getSequence()->push_back(assignNode);
234 TIntermBranch *returnNode = new TIntermBranch(EOpReturn, nullptr);
235 statementList->getSequence()->push_back(returnNode);
236 }
237 else
238 {
239 TIntermBranch *returnNode = new TIntermBranch(EOpReturn, indexNode);
240 statementList->getSequence()->push_back(returnNode);
241 }
242 }
243
244 // Default case
245 TIntermCase *defaultNode = new TIntermCase(nullptr);
246 statementList->getSequence()->push_back(defaultNode);
247 TIntermBranch *breakNode = new TIntermBranch(EOpBreak, nullptr);
248 statementList->getSequence()->push_back(breakNode);
249
250 TIntermSwitch *switchNode = new TIntermSwitch(CreateIndexSymbol(), statementList);
251
Olli Etuaho6d40bbd2016-09-30 13:49:38 +0100252 TIntermBlock *bodyNode = new TIntermBlock();
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300253 bodyNode->getSequence()->push_back(switchNode);
254
Olli Etuaho3272a6d2016-08-29 17:54:50 +0300255 TIntermBinary *cond =
256 new TIntermBinary(EOpLessThan, CreateIndexSymbol(), CreateIntConstantNode(0));
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300257 cond->setType(TType(EbtBool, EbpUndefined));
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300258
259 // Two blocks: one accesses (either reads or writes) the first element and returns,
260 // the other accesses the last element.
Olli Etuaho6d40bbd2016-09-30 13:49:38 +0100261 TIntermBlock *useFirstBlock = new TIntermBlock();
262 TIntermBlock *useLastBlock = new TIntermBlock();
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300263 TIntermBinary *indexFirstNode =
264 CreateIndexDirectBaseSymbolNode(type, fieldType, 0, baseQualifier);
265 TIntermBinary *indexLastNode =
266 CreateIndexDirectBaseSymbolNode(type, fieldType, numCases - 1, baseQualifier);
267 if (write)
268 {
269 TIntermBinary *assignFirstNode = CreateAssignValueSymbolNode(indexFirstNode, fieldType);
270 useFirstBlock->getSequence()->push_back(assignFirstNode);
271 TIntermBranch *returnNode = new TIntermBranch(EOpReturn, nullptr);
272 useFirstBlock->getSequence()->push_back(returnNode);
273
274 TIntermBinary *assignLastNode = CreateAssignValueSymbolNode(indexLastNode, fieldType);
275 useLastBlock->getSequence()->push_back(assignLastNode);
276 }
277 else
278 {
279 TIntermBranch *returnFirstNode = new TIntermBranch(EOpReturn, indexFirstNode);
280 useFirstBlock->getSequence()->push_back(returnFirstNode);
281
282 TIntermBranch *returnLastNode = new TIntermBranch(EOpReturn, indexLastNode);
283 useLastBlock->getSequence()->push_back(returnLastNode);
284 }
Olli Etuaho57961272016-09-14 13:57:46 +0300285 TIntermIfElse *ifNode = new TIntermIfElse(cond, useFirstBlock, nullptr);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300286 bodyNode->getSequence()->push_back(ifNode);
287 bodyNode->getSequence()->push_back(useLastBlock);
288
Olli Etuaho8ad9e752017-01-16 19:55:20 +0000289 TIntermFunctionDefinition *indexingFunction =
290 new TIntermFunctionDefinition(prototypeNode, bodyNode);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300291 return indexingFunction;
292}
293
294class RemoveDynamicIndexingTraverser : public TLValueTrackingTraverser
295{
296 public:
297 RemoveDynamicIndexingTraverser(const TSymbolTable &symbolTable, int shaderVersion);
298
299 bool visitBinary(Visit visit, TIntermBinary *node) override;
300
301 void insertHelperDefinitions(TIntermNode *root);
302
303 void nextIteration();
304
305 bool usedTreeInsertion() const { return mUsedTreeInsertion; }
306
307 protected:
308 // Sets of types that are indexed. Note that these can not store multiple variants
309 // of the same type with different precisions - only one precision gets stored.
310 std::set<TType> mIndexedVecAndMatrixTypes;
311 std::set<TType> mWrittenVecAndMatrixTypes;
312
313 bool mUsedTreeInsertion;
314
315 // When true, the traverser will remove side effects from any indexing expression.
316 // This is done so that in code like
317 // V[j++][i]++.
318 // where V is an array of vectors, j++ will only be evaluated once.
319 bool mRemoveIndexSideEffectsInSubtree;
320};
321
322RemoveDynamicIndexingTraverser::RemoveDynamicIndexingTraverser(const TSymbolTable &symbolTable,
323 int shaderVersion)
324 : TLValueTrackingTraverser(true, false, false, symbolTable, shaderVersion),
325 mUsedTreeInsertion(false),
326 mRemoveIndexSideEffectsInSubtree(false)
327{
328}
329
330void RemoveDynamicIndexingTraverser::insertHelperDefinitions(TIntermNode *root)
331{
Olli Etuaho6d40bbd2016-09-30 13:49:38 +0100332 TIntermBlock *rootBlock = root->getAsBlock();
333 ASSERT(rootBlock != nullptr);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300334 TIntermSequence insertions;
335 for (TType type : mIndexedVecAndMatrixTypes)
336 {
337 insertions.push_back(GetIndexFunctionDefinition(type, false));
338 }
339 for (TType type : mWrittenVecAndMatrixTypes)
340 {
341 insertions.push_back(GetIndexFunctionDefinition(type, true));
342 }
Olli Etuaho6d40bbd2016-09-30 13:49:38 +0100343 mInsertions.push_back(NodeInsertMultipleEntry(rootBlock, 0, insertions, TIntermSequence()));
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300344}
345
346// Create a call to dyn_index_*() based on an indirect indexing op node
347TIntermAggregate *CreateIndexFunctionCall(TIntermBinary *node,
348 TIntermTyped *indexedNode,
349 TIntermTyped *index)
350{
351 ASSERT(node->getOp() == EOpIndexIndirect);
352 TIntermAggregate *indexingCall = new TIntermAggregate(EOpFunctionCall);
353 indexingCall->setLine(node->getLine());
354 indexingCall->setUserDefined();
Olli Etuahobd674552016-10-06 13:28:42 +0100355 indexingCall->getFunctionSymbolInfo()->setNameObj(
356 GetIndexFunctionName(indexedNode->getType(), false));
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300357 indexingCall->getSequence()->push_back(indexedNode);
358 indexingCall->getSequence()->push_back(index);
359
360 TType fieldType = GetFieldType(indexedNode->getType());
361 indexingCall->setType(fieldType);
362 return indexingCall;
363}
364
365TIntermAggregate *CreateIndexedWriteFunctionCall(TIntermBinary *node,
366 TIntermTyped *index,
367 TIntermTyped *writtenValue)
368{
369 // Deep copy the left node so that two pointers to the same node don't end up in the tree.
370 TIntermNode *leftCopy = node->getLeft()->deepCopy();
371 ASSERT(leftCopy != nullptr && leftCopy->getAsTyped() != nullptr);
372 TIntermAggregate *indexedWriteCall =
373 CreateIndexFunctionCall(node, leftCopy->getAsTyped(), index);
Olli Etuahobd674552016-10-06 13:28:42 +0100374 indexedWriteCall->getFunctionSymbolInfo()->setNameObj(
375 GetIndexFunctionName(node->getLeft()->getType(), true));
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300376 indexedWriteCall->setType(TType(EbtVoid));
377 indexedWriteCall->getSequence()->push_back(writtenValue);
378 return indexedWriteCall;
379}
380
381bool RemoveDynamicIndexingTraverser::visitBinary(Visit visit, TIntermBinary *node)
382{
383 if (mUsedTreeInsertion)
384 return false;
385
386 if (node->getOp() == EOpIndexIndirect)
387 {
388 if (mRemoveIndexSideEffectsInSubtree)
389 {
390 ASSERT(node->getRight()->hasSideEffects());
391 // In case we're just removing index side effects, convert
392 // v_expr[index_expr]
393 // to this:
394 // int s0 = index_expr; v_expr[s0];
395 // Now v_expr[s0] can be safely executed several times without unintended side effects.
396
397 // Init the temp variable holding the index
Olli Etuaho13389b62016-10-16 11:48:18 +0100398 TIntermDeclaration *initIndex = createTempInitDeclaration(node->getRight());
Jamie Madill1048e432016-07-23 18:51:28 -0400399 insertStatementInParentBlock(initIndex);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300400 mUsedTreeInsertion = true;
401
402 // Replace the index with the temp variable
403 TIntermSymbol *tempIndex = createTempSymbol(node->getRight()->getType());
Jamie Madill03d863c2016-07-27 18:15:53 -0400404 queueReplacementWithParent(node, node->getRight(), tempIndex, OriginalNode::IS_DROPPED);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300405 }
Jamie Madill666f65a2016-08-26 01:34:37 +0000406 else if (IntermNodePatternMatcher::IsDynamicIndexingOfVectorOrMatrix(node))
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300407 {
408 bool write = isLValueRequiredHere();
409
Jamie Madill666f65a2016-08-26 01:34:37 +0000410#if defined(ANGLE_ENABLE_ASSERTS)
411 // Make sure that IntermNodePatternMatcher is consistent with the slightly differently
412 // implemented checks in this traverser.
413 IntermNodePatternMatcher matcher(
414 IntermNodePatternMatcher::kDynamicIndexingOfVectorOrMatrixInLValue);
415 ASSERT(matcher.match(node, getParentNode(), isLValueRequiredHere()) == write);
416#endif
417
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300418 TType type = node->getLeft()->getType();
419 mIndexedVecAndMatrixTypes.insert(type);
420
421 if (write)
422 {
423 // Convert:
424 // v_expr[index_expr]++;
425 // to this:
426 // int s0 = index_expr; float s1 = dyn_index(v_expr, s0); s1++;
427 // dyn_index_write(v_expr, s0, s1);
428 // This works even if index_expr has some side effects.
429 if (node->getLeft()->hasSideEffects())
430 {
431 // If v_expr has side effects, those need to be removed before proceeding.
432 // Otherwise the side effects of v_expr would be evaluated twice.
433 // The only case where an l-value can have side effects is when it is
434 // indexing. For example, it can be V[j++] where V is an array of vectors.
435 mRemoveIndexSideEffectsInSubtree = true;
436 return true;
437 }
Olli Etuaho8f6eb2a2017-01-12 17:04:58 +0000438
439 TIntermBinary *leftBinary = node->getLeft()->getAsBinaryNode();
440 if (leftBinary != nullptr &&
441 IntermNodePatternMatcher::IsDynamicIndexingOfVectorOrMatrix(leftBinary))
442 {
443 // This is a case like:
444 // mat2 m;
445 // m[a][b]++;
446 // Process the child node m[a] first.
447 return true;
448 }
449
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300450 // TODO(oetuaho@nvidia.com): This is not optimal if the expression using the value
451 // only writes it and doesn't need the previous value. http://anglebug.com/1116
452
453 mWrittenVecAndMatrixTypes.insert(type);
454 TType fieldType = GetFieldType(type);
455
456 TIntermSequence insertionsBefore;
457 TIntermSequence insertionsAfter;
458
459 // Store the index in a temporary signed int variable.
460 TIntermTyped *indexInitializer = EnsureSignedInt(node->getRight());
Olli Etuaho13389b62016-10-16 11:48:18 +0100461 TIntermDeclaration *initIndex = createTempInitDeclaration(indexInitializer);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300462 initIndex->setLine(node->getLine());
463 insertionsBefore.push_back(initIndex);
464
465 TIntermAggregate *indexingCall = CreateIndexFunctionCall(
466 node, node->getLeft(), createTempSymbol(indexInitializer->getType()));
467
468 // Create a node for referring to the index after the nextTemporaryIndex() call
469 // below.
470 TIntermSymbol *tempIndex = createTempSymbol(indexInitializer->getType());
471
472 nextTemporaryIndex(); // From now on, creating temporary symbols that refer to the
473 // field value.
474 insertionsBefore.push_back(createTempInitDeclaration(indexingCall));
475
476 TIntermAggregate *indexedWriteCall =
477 CreateIndexedWriteFunctionCall(node, tempIndex, createTempSymbol(fieldType));
478 insertionsAfter.push_back(indexedWriteCall);
479 insertStatementsInParentBlock(insertionsBefore, insertionsAfter);
Jamie Madill03d863c2016-07-27 18:15:53 -0400480 queueReplacement(node, createTempSymbol(fieldType), OriginalNode::IS_DROPPED);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300481 mUsedTreeInsertion = true;
482 }
483 else
484 {
485 // The indexed value is not being written, so we can simply convert
486 // v_expr[index_expr]
487 // into
488 // dyn_index(v_expr, index_expr)
489 // If the index_expr is unsigned, we'll convert it to signed.
490 ASSERT(!mRemoveIndexSideEffectsInSubtree);
491 TIntermAggregate *indexingCall = CreateIndexFunctionCall(
492 node, node->getLeft(), EnsureSignedInt(node->getRight()));
Jamie Madill03d863c2016-07-27 18:15:53 -0400493 queueReplacement(node, indexingCall, OriginalNode::IS_DROPPED);
Olli Etuaho5d91dda2015-06-18 15:47:46 +0300494 }
495 }
496 }
497 return !mUsedTreeInsertion;
498}
499
500void RemoveDynamicIndexingTraverser::nextIteration()
501{
502 mUsedTreeInsertion = false;
503 mRemoveIndexSideEffectsInSubtree = false;
504 nextTemporaryIndex();
505}
506
507} // namespace
508
509void RemoveDynamicIndexing(TIntermNode *root,
510 unsigned int *temporaryIndex,
511 const TSymbolTable &symbolTable,
512 int shaderVersion)
513{
514 RemoveDynamicIndexingTraverser traverser(symbolTable, shaderVersion);
515 ASSERT(temporaryIndex != nullptr);
516 traverser.useTemporaryIndex(temporaryIndex);
517 do
518 {
519 traverser.nextIteration();
520 root->traverse(&traverser);
521 traverser.updateTree();
522 } while (traverser.usedTreeInsertion());
523 traverser.insertHelperDefinitions(root);
524 traverser.updateTree();
525}
Jamie Madill45bcc782016-11-07 13:58:48 -0500526
527} // namespace sh