1 //
2 // Copyright 2002 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 non-SSBO vectors and
7 // matrices, replacing them with calls to functions that choose which component to return or write.
8 // We don't need to consider dynamic indexing in SSBO since it can be directly as part of the offset
9 // of RWByteAddressBuffer.
10 //
11
12 #include "compiler/translator/tree_ops/RemoveDynamicIndexing.h"
13
14 #include "compiler/translator/Compiler.h"
15 #include "compiler/translator/Diagnostics.h"
16 #include "compiler/translator/InfoSink.h"
17 #include "compiler/translator/StaticType.h"
18 #include "compiler/translator/SymbolTable.h"
19 #include "compiler/translator/tree_util/IntermNodePatternMatcher.h"
20 #include "compiler/translator/tree_util/IntermNode_util.h"
21 #include "compiler/translator/tree_util/IntermTraverse.h"
22
23 namespace sh
24 {
25
26 namespace
27 {
28
29 using DynamicIndexingNodeMatcher = std::function<bool(TIntermBinary *)>;
30
31 const TType *kIndexType = StaticType::Get<EbtInt, EbpHigh, EvqIn, 1, 1>();
32
33 constexpr const ImmutableString kBaseName("base");
34 constexpr const ImmutableString kIndexName("index");
35 constexpr const ImmutableString kValueName("value");
36
GetIndexFunctionName(const TType & type,bool write)37 std::string GetIndexFunctionName(const TType &type, bool write)
38 {
39 TInfoSinkBase nameSink;
40 nameSink << "dyn_index_";
41 if (write)
42 {
43 nameSink << "write_";
44 }
45 if (type.isMatrix())
46 {
47 nameSink << "mat" << type.getCols() << "x" << type.getRows();
48 }
49 else
50 {
51 switch (type.getBasicType())
52 {
53 case EbtInt:
54 nameSink << "ivec";
55 break;
56 case EbtBool:
57 nameSink << "bvec";
58 break;
59 case EbtUInt:
60 nameSink << "uvec";
61 break;
62 case EbtFloat:
63 nameSink << "vec";
64 break;
65 default:
66 UNREACHABLE();
67 }
68 nameSink << type.getNominalSize();
69 }
70 return nameSink.str();
71 }
72
CreateIntConstantNode(int i)73 TIntermConstantUnion *CreateIntConstantNode(int i)
74 {
75 TConstantUnion *constant = new TConstantUnion();
76 constant->setIConst(i);
77 return new TIntermConstantUnion(constant, TType(EbtInt, EbpHigh));
78 }
79
EnsureSignedInt(TIntermTyped * node)80 TIntermTyped *EnsureSignedInt(TIntermTyped *node)
81 {
82 if (node->getBasicType() == EbtInt)
83 return node;
84
85 TIntermSequence arguments;
86 arguments.push_back(node);
87 return TIntermAggregate::CreateConstructor(TType(EbtInt), &arguments);
88 }
89
GetFieldType(const TType & indexedType)90 TType *GetFieldType(const TType &indexedType)
91 {
92 if (indexedType.isMatrix())
93 {
94 TType *fieldType = new TType(indexedType.getBasicType(), indexedType.getPrecision());
95 fieldType->setPrimarySize(static_cast<unsigned char>(indexedType.getRows()));
96 return fieldType;
97 }
98 else
99 {
100 return new TType(indexedType.getBasicType(), indexedType.getPrecision());
101 }
102 }
103
GetBaseType(const TType & type,bool write)104 const TType *GetBaseType(const TType &type, bool write)
105 {
106 TType *baseType = new TType(type);
107 // Conservatively use highp here, even if the indexed type is not highp. That way the code can't
108 // end up using mediump version of an indexing function for a highp value, if both mediump and
109 // highp values are being indexed in the shader. For HLSL precision doesn't matter, but in
110 // principle this code could be used with multiple backends.
111 baseType->setPrecision(EbpHigh);
112 baseType->setQualifier(EvqInOut);
113 if (!write)
114 baseType->setQualifier(EvqIn);
115 return baseType;
116 }
117
118 // Generate a read or write function for one field in a vector/matrix.
119 // Out-of-range indices are clamped. This is consistent with how ANGLE handles out-of-range
120 // indices in other places.
121 // Note that indices can be either int or uint. We create only int versions of the functions,
122 // and convert uint indices to int at the call site.
123 // read function example:
124 // float dyn_index_vec2(in vec2 base, in int index)
125 // {
126 // switch(index)
127 // {
128 // case (0):
129 // return base[0];
130 // case (1):
131 // return base[1];
132 // default:
133 // break;
134 // }
135 // if (index < 0)
136 // return base[0];
137 // return base[1];
138 // }
139 // write function example:
140 // void dyn_index_write_vec2(inout vec2 base, in int index, in float value)
141 // {
142 // switch(index)
143 // {
144 // case (0):
145 // base[0] = value;
146 // return;
147 // case (1):
148 // base[1] = value;
149 // return;
150 // default:
151 // break;
152 // }
153 // if (index < 0)
154 // {
155 // base[0] = value;
156 // return;
157 // }
158 // base[1] = value;
159 // }
160 // Note that else is not used in above functions to avoid the RewriteElseBlocks transformation.
GetIndexFunctionDefinition(const TType & type,bool write,const TFunction & func,TSymbolTable * symbolTable)161 TIntermFunctionDefinition *GetIndexFunctionDefinition(const TType &type,
162 bool write,
163 const TFunction &func,
164 TSymbolTable *symbolTable)
165 {
166 ASSERT(!type.isArray());
167
168 int numCases = 0;
169 if (type.isMatrix())
170 {
171 numCases = type.getCols();
172 }
173 else
174 {
175 numCases = type.getNominalSize();
176 }
177
178 std::string functionName = GetIndexFunctionName(type, write);
179 TIntermFunctionPrototype *prototypeNode = CreateInternalFunctionPrototypeNode(func);
180
181 TIntermSymbol *baseParam = new TIntermSymbol(func.getParam(0));
182 TIntermSymbol *indexParam = new TIntermSymbol(func.getParam(1));
183 TIntermSymbol *valueParam = nullptr;
184 if (write)
185 {
186 valueParam = new TIntermSymbol(func.getParam(2));
187 }
188
189 TIntermBlock *statementList = new TIntermBlock();
190 for (int i = 0; i < numCases; ++i)
191 {
192 TIntermCase *caseNode = new TIntermCase(CreateIntConstantNode(i));
193 statementList->getSequence()->push_back(caseNode);
194
195 TIntermBinary *indexNode =
196 new TIntermBinary(EOpIndexDirect, baseParam->deepCopy(), CreateIndexNode(i));
197 if (write)
198 {
199 TIntermBinary *assignNode =
200 new TIntermBinary(EOpAssign, indexNode, valueParam->deepCopy());
201 statementList->getSequence()->push_back(assignNode);
202 TIntermBranch *returnNode = new TIntermBranch(EOpReturn, nullptr);
203 statementList->getSequence()->push_back(returnNode);
204 }
205 else
206 {
207 TIntermBranch *returnNode = new TIntermBranch(EOpReturn, indexNode);
208 statementList->getSequence()->push_back(returnNode);
209 }
210 }
211
212 // Default case
213 TIntermCase *defaultNode = new TIntermCase(nullptr);
214 statementList->getSequence()->push_back(defaultNode);
215 TIntermBranch *breakNode = new TIntermBranch(EOpBreak, nullptr);
216 statementList->getSequence()->push_back(breakNode);
217
218 TIntermSwitch *switchNode = new TIntermSwitch(indexParam->deepCopy(), statementList);
219
220 TIntermBlock *bodyNode = new TIntermBlock();
221 bodyNode->getSequence()->push_back(switchNode);
222
223 TIntermBinary *cond =
224 new TIntermBinary(EOpLessThan, indexParam->deepCopy(), CreateIntConstantNode(0));
225
226 // Two blocks: one accesses (either reads or writes) the first element and returns,
227 // the other accesses the last element.
228 TIntermBlock *useFirstBlock = new TIntermBlock();
229 TIntermBlock *useLastBlock = new TIntermBlock();
230 TIntermBinary *indexFirstNode =
231 new TIntermBinary(EOpIndexDirect, baseParam->deepCopy(), CreateIndexNode(0));
232 TIntermBinary *indexLastNode =
233 new TIntermBinary(EOpIndexDirect, baseParam->deepCopy(), CreateIndexNode(numCases - 1));
234 if (write)
235 {
236 TIntermBinary *assignFirstNode =
237 new TIntermBinary(EOpAssign, indexFirstNode, valueParam->deepCopy());
238 useFirstBlock->getSequence()->push_back(assignFirstNode);
239 TIntermBranch *returnNode = new TIntermBranch(EOpReturn, nullptr);
240 useFirstBlock->getSequence()->push_back(returnNode);
241
242 TIntermBinary *assignLastNode =
243 new TIntermBinary(EOpAssign, indexLastNode, valueParam->deepCopy());
244 useLastBlock->getSequence()->push_back(assignLastNode);
245 }
246 else
247 {
248 TIntermBranch *returnFirstNode = new TIntermBranch(EOpReturn, indexFirstNode);
249 useFirstBlock->getSequence()->push_back(returnFirstNode);
250
251 TIntermBranch *returnLastNode = new TIntermBranch(EOpReturn, indexLastNode);
252 useLastBlock->getSequence()->push_back(returnLastNode);
253 }
254 TIntermIfElse *ifNode = new TIntermIfElse(cond, useFirstBlock, nullptr);
255 bodyNode->getSequence()->push_back(ifNode);
256 bodyNode->getSequence()->push_back(useLastBlock);
257
258 TIntermFunctionDefinition *indexingFunction =
259 new TIntermFunctionDefinition(prototypeNode, bodyNode);
260 return indexingFunction;
261 }
262
263 class RemoveDynamicIndexingTraverser : public TLValueTrackingTraverser
264 {
265 public:
266 RemoveDynamicIndexingTraverser(DynamicIndexingNodeMatcher &&matcher,
267 TSymbolTable *symbolTable,
268 PerformanceDiagnostics *perfDiagnostics);
269
270 bool visitBinary(Visit visit, TIntermBinary *node) override;
271
272 void insertHelperDefinitions(TIntermNode *root);
273
274 void nextIteration();
275
usedTreeInsertion() const276 bool usedTreeInsertion() const { return mUsedTreeInsertion; }
277
278 protected:
279 // Maps of types that are indexed to the indexing function ids used for them. Note that these
280 // can not store multiple variants of the same type with different precisions - only one
281 // precision gets stored.
282 std::map<TType, TFunction *> mIndexedVecAndMatrixTypes;
283 std::map<TType, TFunction *> mWrittenVecAndMatrixTypes;
284
285 bool mUsedTreeInsertion;
286
287 // When true, the traverser will remove side effects from any indexing expression.
288 // This is done so that in code like
289 // V[j++][i]++.
290 // where V is an array of vectors, j++ will only be evaluated once.
291 bool mRemoveIndexSideEffectsInSubtree;
292
293 DynamicIndexingNodeMatcher mMatcher;
294 PerformanceDiagnostics *mPerfDiagnostics;
295 };
296
RemoveDynamicIndexingTraverser(DynamicIndexingNodeMatcher && matcher,TSymbolTable * symbolTable,PerformanceDiagnostics * perfDiagnostics)297 RemoveDynamicIndexingTraverser::RemoveDynamicIndexingTraverser(
298 DynamicIndexingNodeMatcher &&matcher,
299 TSymbolTable *symbolTable,
300 PerformanceDiagnostics *perfDiagnostics)
301 : TLValueTrackingTraverser(true, false, false, symbolTable),
302 mUsedTreeInsertion(false),
303 mRemoveIndexSideEffectsInSubtree(false),
304 mMatcher(matcher),
305 mPerfDiagnostics(perfDiagnostics)
306 {}
307
insertHelperDefinitions(TIntermNode * root)308 void RemoveDynamicIndexingTraverser::insertHelperDefinitions(TIntermNode *root)
309 {
310 TIntermBlock *rootBlock = root->getAsBlock();
311 ASSERT(rootBlock != nullptr);
312 TIntermSequence insertions;
313 for (auto &type : mIndexedVecAndMatrixTypes)
314 {
315 insertions.push_back(
316 GetIndexFunctionDefinition(type.first, false, *type.second, mSymbolTable));
317 }
318 for (auto &type : mWrittenVecAndMatrixTypes)
319 {
320 insertions.push_back(
321 GetIndexFunctionDefinition(type.first, true, *type.second, mSymbolTable));
322 }
323 rootBlock->insertChildNodes(0, insertions);
324 }
325
326 // Create a call to dyn_index_*() based on an indirect indexing op node
CreateIndexFunctionCall(TIntermBinary * node,TIntermTyped * index,TFunction * indexingFunction)327 TIntermAggregate *CreateIndexFunctionCall(TIntermBinary *node,
328 TIntermTyped *index,
329 TFunction *indexingFunction)
330 {
331 ASSERT(node->getOp() == EOpIndexIndirect);
332 TIntermSequence arguments;
333 arguments.push_back(node->getLeft());
334 arguments.push_back(index);
335
336 TIntermAggregate *indexingCall =
337 TIntermAggregate::CreateFunctionCall(*indexingFunction, &arguments);
338 indexingCall->setLine(node->getLine());
339 return indexingCall;
340 }
341
CreateIndexedWriteFunctionCall(TIntermBinary * node,TVariable * index,TVariable * writtenValue,TFunction * indexedWriteFunction)342 TIntermAggregate *CreateIndexedWriteFunctionCall(TIntermBinary *node,
343 TVariable *index,
344 TVariable *writtenValue,
345 TFunction *indexedWriteFunction)
346 {
347 ASSERT(node->getOp() == EOpIndexIndirect);
348 TIntermSequence arguments;
349 // Deep copy the child nodes so that two pointers to the same node don't end up in the tree.
350 arguments.push_back(node->getLeft()->deepCopy());
351 arguments.push_back(CreateTempSymbolNode(index));
352 arguments.push_back(CreateTempSymbolNode(writtenValue));
353
354 TIntermAggregate *indexedWriteCall =
355 TIntermAggregate::CreateFunctionCall(*indexedWriteFunction, &arguments);
356 indexedWriteCall->setLine(node->getLine());
357 return indexedWriteCall;
358 }
359
visitBinary(Visit visit,TIntermBinary * node)360 bool RemoveDynamicIndexingTraverser::visitBinary(Visit visit, TIntermBinary *node)
361 {
362 if (mUsedTreeInsertion)
363 return false;
364
365 if (node->getOp() == EOpIndexIndirect)
366 {
367 if (mRemoveIndexSideEffectsInSubtree)
368 {
369 ASSERT(node->getRight()->hasSideEffects());
370 // In case we're just removing index side effects, convert
371 // v_expr[index_expr]
372 // to this:
373 // int s0 = index_expr; v_expr[s0];
374 // Now v_expr[s0] can be safely executed several times without unintended side effects.
375 TIntermDeclaration *indexVariableDeclaration = nullptr;
376 TVariable *indexVariable = DeclareTempVariable(mSymbolTable, node->getRight(),
377 EvqTemporary, &indexVariableDeclaration);
378 insertStatementInParentBlock(indexVariableDeclaration);
379 mUsedTreeInsertion = true;
380
381 // Replace the index with the temp variable
382 TIntermSymbol *tempIndex = CreateTempSymbolNode(indexVariable);
383 queueReplacementWithParent(node, node->getRight(), tempIndex, OriginalNode::IS_DROPPED);
384 }
385 else if (mMatcher(node))
386 {
387 if (mPerfDiagnostics)
388 {
389 mPerfDiagnostics->warning(node->getLine(),
390 "Performance: dynamic indexing of vectors and "
391 "matrices is emulated and can be slow.",
392 "[]");
393 }
394 bool write = isLValueRequiredHere();
395
396 #if defined(ANGLE_ENABLE_ASSERTS)
397 // Make sure that IntermNodePatternMatcher is consistent with the slightly differently
398 // implemented checks in this traverser.
399 IntermNodePatternMatcher matcher(
400 IntermNodePatternMatcher::kDynamicIndexingOfVectorOrMatrixInLValue);
401 ASSERT(matcher.match(node, getParentNode(), isLValueRequiredHere()) == write);
402 #endif
403
404 const TType &type = node->getLeft()->getType();
405 ImmutableString indexingFunctionName(GetIndexFunctionName(type, false));
406 TFunction *indexingFunction = nullptr;
407 if (mIndexedVecAndMatrixTypes.find(type) == mIndexedVecAndMatrixTypes.end())
408 {
409 indexingFunction =
410 new TFunction(mSymbolTable, indexingFunctionName, SymbolType::AngleInternal,
411 GetFieldType(type), true);
412 indexingFunction->addParameter(new TVariable(
413 mSymbolTable, kBaseName, GetBaseType(type, false), SymbolType::AngleInternal));
414 indexingFunction->addParameter(
415 new TVariable(mSymbolTable, kIndexName, kIndexType, SymbolType::AngleInternal));
416 mIndexedVecAndMatrixTypes[type] = indexingFunction;
417 }
418 else
419 {
420 indexingFunction = mIndexedVecAndMatrixTypes[type];
421 }
422
423 if (write)
424 {
425 // Convert:
426 // v_expr[index_expr]++;
427 // to this:
428 // int s0 = index_expr; float s1 = dyn_index(v_expr, s0); s1++;
429 // dyn_index_write(v_expr, s0, s1);
430 // This works even if index_expr has some side effects.
431 if (node->getLeft()->hasSideEffects())
432 {
433 // If v_expr has side effects, those need to be removed before proceeding.
434 // Otherwise the side effects of v_expr would be evaluated twice.
435 // The only case where an l-value can have side effects is when it is
436 // indexing. For example, it can be V[j++] where V is an array of vectors.
437 mRemoveIndexSideEffectsInSubtree = true;
438 return true;
439 }
440
441 TIntermBinary *leftBinary = node->getLeft()->getAsBinaryNode();
442 if (leftBinary != nullptr && mMatcher(leftBinary))
443 {
444 // This is a case like:
445 // mat2 m;
446 // m[a][b]++;
447 // Process the child node m[a] first.
448 return true;
449 }
450
451 // TODO(oetuaho@nvidia.com): This is not optimal if the expression using the value
452 // only writes it and doesn't need the previous value. http://anglebug.com/1116
453
454 TFunction *indexedWriteFunction = nullptr;
455 if (mWrittenVecAndMatrixTypes.find(type) == mWrittenVecAndMatrixTypes.end())
456 {
457 ImmutableString functionName(
458 GetIndexFunctionName(node->getLeft()->getType(), true));
459 indexedWriteFunction =
460 new TFunction(mSymbolTable, functionName, SymbolType::AngleInternal,
461 StaticType::GetBasic<EbtVoid>(), false);
462 indexedWriteFunction->addParameter(new TVariable(mSymbolTable, kBaseName,
463 GetBaseType(type, true),
464 SymbolType::AngleInternal));
465 indexedWriteFunction->addParameter(new TVariable(
466 mSymbolTable, kIndexName, kIndexType, SymbolType::AngleInternal));
467 TType *valueType = GetFieldType(type);
468 valueType->setQualifier(EvqIn);
469 indexedWriteFunction->addParameter(new TVariable(
470 mSymbolTable, kValueName, static_cast<const TType *>(valueType),
471 SymbolType::AngleInternal));
472 mWrittenVecAndMatrixTypes[type] = indexedWriteFunction;
473 }
474 else
475 {
476 indexedWriteFunction = mWrittenVecAndMatrixTypes[type];
477 }
478
479 TIntermSequence insertionsBefore;
480 TIntermSequence insertionsAfter;
481
482 // Store the index in a temporary signed int variable.
483 // s0 = index_expr;
484 TIntermTyped *indexInitializer = EnsureSignedInt(node->getRight());
485 TIntermDeclaration *indexVariableDeclaration = nullptr;
486 TVariable *indexVariable = DeclareTempVariable(
487 mSymbolTable, indexInitializer, EvqTemporary, &indexVariableDeclaration);
488 insertionsBefore.push_back(indexVariableDeclaration);
489
490 // s1 = dyn_index(v_expr, s0);
491 TIntermAggregate *indexingCall = CreateIndexFunctionCall(
492 node, CreateTempSymbolNode(indexVariable), indexingFunction);
493 TIntermDeclaration *fieldVariableDeclaration = nullptr;
494 TVariable *fieldVariable = DeclareTempVariable(
495 mSymbolTable, indexingCall, EvqTemporary, &fieldVariableDeclaration);
496 insertionsBefore.push_back(fieldVariableDeclaration);
497
498 // dyn_index_write(v_expr, s0, s1);
499 TIntermAggregate *indexedWriteCall = CreateIndexedWriteFunctionCall(
500 node, indexVariable, fieldVariable, indexedWriteFunction);
501 insertionsAfter.push_back(indexedWriteCall);
502 insertStatementsInParentBlock(insertionsBefore, insertionsAfter);
503
504 // replace the node with s1
505 queueReplacement(CreateTempSymbolNode(fieldVariable), OriginalNode::IS_DROPPED);
506 mUsedTreeInsertion = true;
507 }
508 else
509 {
510 // The indexed value is not being written, so we can simply convert
511 // v_expr[index_expr]
512 // into
513 // dyn_index(v_expr, index_expr)
514 // If the index_expr is unsigned, we'll convert it to signed.
515 ASSERT(!mRemoveIndexSideEffectsInSubtree);
516 TIntermAggregate *indexingCall = CreateIndexFunctionCall(
517 node, EnsureSignedInt(node->getRight()), indexingFunction);
518 queueReplacement(indexingCall, OriginalNode::IS_DROPPED);
519 }
520 }
521 }
522 return !mUsedTreeInsertion;
523 }
524
nextIteration()525 void RemoveDynamicIndexingTraverser::nextIteration()
526 {
527 mUsedTreeInsertion = false;
528 mRemoveIndexSideEffectsInSubtree = false;
529 }
530
RemoveDynamicIndexingIf(DynamicIndexingNodeMatcher && matcher,TCompiler * compiler,TIntermNode * root,TSymbolTable * symbolTable,PerformanceDiagnostics * perfDiagnostics)531 bool RemoveDynamicIndexingIf(DynamicIndexingNodeMatcher &&matcher,
532 TCompiler *compiler,
533 TIntermNode *root,
534 TSymbolTable *symbolTable,
535 PerformanceDiagnostics *perfDiagnostics)
536 {
537 RemoveDynamicIndexingTraverser traverser(std::move(matcher), symbolTable, perfDiagnostics);
538 do
539 {
540 traverser.nextIteration();
541 root->traverse(&traverser);
542 if (!traverser.updateTree(compiler, root))
543 {
544 return false;
545 }
546 } while (traverser.usedTreeInsertion());
547 // TODO(oetuaho@nvidia.com): It might be nicer to add the helper definitions also in the middle
548 // of traversal. Now the tree ends up in an inconsistent state in the middle, since there are
549 // function call nodes with no corresponding definition nodes. This needs special handling in
550 // TIntermLValueTrackingTraverser, and creates intricacies that are not easily apparent from a
551 // superficial reading of the code.
552 traverser.insertHelperDefinitions(root);
553 return compiler->validateAST(root);
554 }
555
556 } // namespace
557
RemoveDynamicIndexingOfNonSSBOVectorOrMatrix(TCompiler * compiler,TIntermNode * root,TSymbolTable * symbolTable,PerformanceDiagnostics * perfDiagnostics)558 ANGLE_NO_DISCARD bool RemoveDynamicIndexingOfNonSSBOVectorOrMatrix(
559 TCompiler *compiler,
560 TIntermNode *root,
561 TSymbolTable *symbolTable,
562 PerformanceDiagnostics *perfDiagnostics)
563 {
564 DynamicIndexingNodeMatcher matcher = [](TIntermBinary *node) {
565 return IntermNodePatternMatcher::IsDynamicIndexingOfNonSSBOVectorOrMatrix(node);
566 };
567 return RemoveDynamicIndexingIf(std::move(matcher), compiler, root, symbolTable,
568 perfDiagnostics);
569 }
570
RemoveDynamicIndexingOfSwizzledVector(TCompiler * compiler,TIntermNode * root,TSymbolTable * symbolTable,PerformanceDiagnostics * perfDiagnostics)571 ANGLE_NO_DISCARD bool RemoveDynamicIndexingOfSwizzledVector(TCompiler *compiler,
572 TIntermNode *root,
573 TSymbolTable *symbolTable,
574 PerformanceDiagnostics *perfDiagnostics)
575 {
576 DynamicIndexingNodeMatcher matcher = [](TIntermBinary *node) {
577 return IntermNodePatternMatcher::IsDynamicIndexingOfSwizzledVector(node);
578 };
579 return RemoveDynamicIndexingIf(std::move(matcher), compiler, root, symbolTable,
580 perfDiagnostics);
581 }
582
583 } // namespace sh
584