source: icGREP/icgrep-devel/icgrep/pablo/optimizers/pablo_simplifier.cpp @ 5933

Last change on this file since 5933 was 5933, checked in by cameron, 11 months ago

Including IR/DerivedTypes.h for missing inlines: getSequentialElementType

File size: 24.0 KB
Line 
1#include <pablo/optimizers/pablo_simplifier.hpp>
2#include <pablo/pablo_kernel.h>
3#include <pablo/codegenstate.h>
4#include <pablo/expression_map.hpp>
5#include <pablo/boolean.h>
6#include <pablo/pe_zeroes.h>
7#include <pablo/pe_ones.h>
8#include <pablo/arithmetic.h>
9#include <pablo/branch.h>
10#include <pablo/ps_assign.h>
11#include <pablo/pe_advance.h>
12#include <pablo/pe_lookahead.h>
13#include <pablo/pe_scanthru.h>
14#include <pablo/pe_matchstar.h>
15#include <pablo/pe_var.h>
16#ifndef NDEBUG
17#include <pablo/analysis/pabloverifier.hpp>
18#endif
19#include <boost/container/flat_set.hpp>
20#include <llvm/IR/Type.h>
21#include <llvm/IR/DerivedTypes.h>  // for get getSequentialElementType
22
23#include <llvm/Support/raw_ostream.h>
24
25using namespace boost;
26using namespace boost::container;
27using namespace llvm;
28
29namespace pablo {
30
31using TypeId = PabloAST::ClassTypeId;
32
33using ScopeMap = flat_map<PabloBlock *, unsigned>;
34
35/** ------------------------------------------------------------------------------------------------------------- *
36 * @brief VariableTable
37 ** ------------------------------------------------------------------------------------------------------------- */
38struct VariableTable {
39
40    VariableTable(VariableTable * predecessor = nullptr)
41    : mPredecessor(predecessor) {
42
43    }
44
45    PabloAST * get(PabloAST * const var) const {
46        const auto f = mMap.find(var);
47        if (f == mMap.end()) {
48            return (mPredecessor) ? mPredecessor->get(var) : nullptr;
49        }
50        return f->second;
51    }
52
53    void put(PabloAST * const var, PabloAST * value) {
54        const auto f = mMap.find(var);
55        if (LLVM_LIKELY(f == mMap.end())) {
56            mMap.emplace(var, value);
57        } else {
58            f->second = value;
59        }
60        assert (get(var) == value);
61    }
62
63private:
64    VariableTable * const mPredecessor;
65    flat_map<PabloAST *, PabloAST *> mMap;
66};
67
68struct PassContainer {
69
70/** ------------------------------------------------------------------------------------------------------------- *
71 * @brief run
72 ** ------------------------------------------------------------------------------------------------------------- */
73void run(PabloKernel * const kernel) {
74    redundancyElimination(kernel->getEntryScope(), nullptr, nullptr);
75    strengthReduction(kernel->getEntryScope());
76    deadCodeElimination(kernel->getEntryScope());
77}
78
79protected:
80
81/** ------------------------------------------------------------------------------------------------------------- *
82 * @brief redundancyElimination
83 *
84 * Note: Do not recursively delete statements in this function. The ExpressionTable could use deleted statements
85 * as replacements. Let the DCE remove the unnecessary statements with the finalized Def-Use information.
86 ** ------------------------------------------------------------------------------------------------------------- */
87void redundancyElimination(PabloBlock * const block, ExpressionTable * const et, VariableTable * const vt) {
88    ExpressionTable expressions(et);
89    VariableTable variables(vt);
90
91    if (Branch * br = block->getBranch()) {
92        assert ("block has a branch but the expression and variable tables were not supplied" && et && vt);
93        for (Var * var : br->getEscaped()) {
94            variables.put(var, var);
95        }
96    }
97
98    mInScope.push_back(block);
99
100    const auto baseNonZeroEntries = mNonZero.size();
101    Statement * stmt = block->front();
102    while (stmt) {
103
104        if (LLVM_UNLIKELY(isa<Assign>(stmt))) {
105            Assign * const assign = cast<Assign>(stmt);
106            PabloAST * const var = assign->getVariable();
107            PabloAST * value = assign->getValue();
108            if (LLVM_UNLIKELY(var == value)) {
109                stmt = stmt->eraseFromParent();
110                continue;
111            }
112            while (LLVM_UNLIKELY(isa<Var>(value))) {
113                PabloAST * next = variables.get(cast<Var>(value));
114                if (LLVM_LIKELY(next == nullptr || next == value)) {
115                    break;
116                }
117                value = next;
118                assign->setValue(value);
119            }
120            if (LLVM_UNLIKELY(variables.get(var) == value)) {
121                stmt = stmt->eraseFromParent();
122                continue;
123            }
124            variables.put(var, value);
125
126        } else if (LLVM_UNLIKELY(isa<Branch>(stmt))) {
127
128            Branch * const br = cast<Branch>(stmt);
129            PabloAST * cond = br->getCondition();
130            if (isa<Var>(cond)) {
131                PabloAST * const value = variables.get(cast<Var>(cond));
132                if (value) {
133                    cond = value;
134                    if (isa<If>(br)) {
135                        br->setCondition(cond);
136                    }
137                }
138            }
139
140            // Test whether we can ever take this branch
141            if (LLVM_UNLIKELY(isa<Zeroes>(cond))) {
142                stmt = stmt->eraseFromParent();
143                continue;
144            }
145
146            // If we're guaranteed to take this branch, flatten it.
147            if (LLVM_LIKELY(isa<If>(br)) && LLVM_UNLIKELY(isNonZero(cond))) {
148                stmt = flatten(br);
149                continue;
150            }
151
152            // Mark the cond as non-zero prior to processing the inner scope.
153            mNonZero.push_back(cond);
154            // Process the Branch body
155            redundancyElimination(br->getBody(), &expressions, &variables);
156            assert (mNonZero.back() == cond);
157            mNonZero.pop_back();
158
159            if (LLVM_LIKELY(isa<If>(br))) {
160                // Check whether the cost of testing the condition and taking the branch with
161                // 100% correct prediction rate exceeds the cost of the body itself
162                if (LLVM_UNLIKELY(isTrivial(br->getBody()))) {
163                    stmt = flatten(br);
164                    continue;
165                }
166            }
167
168        } else {
169
170            // demote any uses of any Var whose value is in scope
171            for (unsigned i = 0; i < stmt->getNumOperands(); ++i) {
172                PabloAST * op = stmt->getOperand(i);
173                if (LLVM_UNLIKELY(isa<Var>(op))) {
174                    PabloAST * const value = variables.get(cast<Var>(op));
175                    if (value && value != op) {
176                        stmt->setOperand(i, value);
177                    }
178                }
179            }
180
181            PabloAST * const folded = triviallyFold(stmt, block);
182            if (folded) {
183                Statement * const prior = stmt->getPrevNode();
184                stmt->replaceWith(folded);
185                stmt = prior ? prior->getNextNode() : block->front();
186                continue;
187            }
188
189            // By recording which statements have already been seen, we can detect the redundant statements
190            // as any having the same type and operands. If so, we can replace its users with the prior statement.
191            // and erase this statement from the AST
192            const auto f = expressions.findOrAdd(stmt);
193            if (!f.second) {
194                stmt = stmt->replaceWith(f.first);
195                continue;
196            }
197
198            // Attempt to extend our set of trivially non-zero statements.
199            if (isa<Or>(stmt)) {
200                for (unsigned i = 0; i < stmt->getNumOperands(); ++i) {
201                    if (LLVM_UNLIKELY(isNonZero(stmt->getOperand(i)))) {
202                        mNonZero.push_back(stmt);
203                        break;
204                    }
205                }
206            } else if (isa<Advance>(stmt)) {
207                const Advance * const adv = cast<Advance>(stmt);
208                if (LLVM_LIKELY(adv->getAmount() < (adv->getType()->getPrimitiveSizeInBits() / 2))) {
209                    if (LLVM_UNLIKELY(isNonZero(adv->getExpression()))) {
210                        mNonZero.push_back(adv);
211                    }
212                }
213            }
214        }
215
216        stmt = stmt->getNextNode();
217    }
218
219    // Erase any local non-zero entries that were discovered while processing this scope
220    mNonZero.erase(mNonZero.begin() + baseNonZeroEntries, mNonZero.end());
221
222    assert (mInScope.back() == block);
223    mInScope.pop_back();
224
225    // If this block has a branch statement leading into it, we can verify whether an escaped value
226    // was updated within this block and update the preceeding block's variable state appropriately.
227
228    Branch * const br = block->getBranch();
229    if (LLVM_LIKELY(br != nullptr)) {
230
231        // When removing identical escaped values, we have to consider that the identical Vars could
232        // be assigned new differing values later in the outer body. Thus instead of replacing them
233        // directly, we map future uses of the duplicate Var to the initial one. The DCE pass will
234        // later mark any Assign statement as dead if the Var is never read.
235
236        const auto escaped = br->getEscaped();
237        const auto n = escaped.size();
238        PabloAST * variable[n];
239        PabloAST * incoming[n];
240        PabloAST * outgoing[n];
241        for (unsigned i = 0; i < n; ++i) {
242            variable[i] = escaped[i];
243            incoming[i] = vt->get(variable[i]);
244            outgoing[i] = variables.get(variable[i]);
245            if (LLVM_UNLIKELY(incoming[i] == outgoing[i])) {
246                variable[i] = incoming[i];
247            } else {
248                for (unsigned j = 0; j < i; ++j) {
249                    if (LLVM_UNLIKELY((outgoing[j] == outgoing[i]) && (incoming[j] == incoming[i]))) {
250                        variable[i] = variable[j];
251                        break;
252                    }
253                }
254            }
255            vt->put(escaped[i], variable[i]);
256        }
257
258    }
259
260}
261
262
263/** ------------------------------------------------------------------------------------------------------------- *
264 * @brief fold
265 ** ------------------------------------------------------------------------------------------------------------- */
266static PabloAST * triviallyFold(Statement * stmt, PabloBlock * const block) {
267    if (isa<Not>(stmt)) {
268        PabloAST * value = stmt->getOperand(0);
269        if (LLVM_UNLIKELY(isa<Not>(value))) {
270            return cast<Not>(value)->getOperand(0); // ¬¬A ⇔ A
271        } else if (LLVM_UNLIKELY(isa<Zeroes>(value))) {
272            return block->createOnes(stmt->getType()); // ¬0 ⇔ 1
273        }  else if (LLVM_UNLIKELY(isa<Ones>(value))) {
274            return block->createZeroes(stmt->getType()); // ¬1 ⇔ 0
275        }
276    } else if (isa<And>(stmt) || isa<Or>(stmt)) {
277        PabloAST * op[2];
278        op[0] = stmt->getOperand(0);
279        op[1] = stmt->getOperand(1);
280        for (unsigned i = 0; i < 2; ++i) {
281            if (const Not * const n = dyn_cast<Not>(op[i])) {
282                if (LLVM_UNLIKELY(n->getExpr() == op[1 - i])) {
283                    if (isa<And>(stmt)) {
284                        return block->createZeroes(stmt->getType());
285                    } else {
286                        return block->createOnes(stmt->getType());
287                    }
288                }
289            } else if (LLVM_UNLIKELY(isa<Zeroes>(op[i]) || isa<Ones>(op[i]))) {
290                if (isa<And>(stmt) ^ isa<Zeroes>(op)) {
291                    return op[1 - i];
292                } else {
293                    return op[i];
294                }
295            }
296        }
297        if (LLVM_UNLIKELY(op[0] == op[1])) {
298            return op[0];
299        } else {
300            if (op[1] < op[0]) {
301                stmt->setOperand(0, op[1]);
302                stmt->setOperand(1, op[0]);
303            }
304            return nullptr;
305        }
306    } else if (isa<Xor>(stmt)) {
307        PabloAST * op[2];
308        op[0] = stmt->getOperand(0);
309        op[1] = stmt->getOperand(1);
310        bool negated = false;
311        PabloAST * expr = nullptr;
312        for (unsigned i = 0; i < 2; ++i) {
313            if (Not * const n = dyn_cast<Not>(op[i])) {
314                negated ^= true;
315                op[i] = n->getExpr();
316            } else if (LLVM_UNLIKELY(isa<Zeroes>(op[i]) || isa<Ones>(op[i]))) {
317                negated ^= isa<Ones>(op);
318                expr = op[1 - i];
319            }
320        }
321        if (LLVM_LIKELY(expr == nullptr)) {
322            if (LLVM_UNLIKELY(op[0] == op[1])) {
323                if (LLVM_UNLIKELY(negated)) {
324                    return block->createOnes(stmt->getType());
325                } else {
326                    return block->createZeroes(stmt->getType());
327                }
328            } else {
329                if (op[1] < op[0]) {
330                    std::swap(op[0], op[1]);
331                }
332                stmt->setOperand(0, op[0]);
333                stmt->setOperand(1, op[1]);
334            }
335        }
336        if (LLVM_UNLIKELY(negated)) {
337            block->setInsertPoint(stmt);
338            expr = triviallyFold(block->createNot(expr ? expr : stmt), block);
339        }
340        return expr;
341    } else if (isa<Advance>(stmt)) {
342        Advance * const adv = cast<Advance>(stmt);
343        if (LLVM_UNLIKELY(isa<Zeroes>(adv->getExpression()) || adv->getAmount() == 0)) {
344            return adv->getExpression();
345        }
346    } else if (isa<ScanThru>(stmt)) {
347        ScanThru * const st = cast<ScanThru>(stmt);
348        if (LLVM_UNLIKELY(isa<Zeroes>(st->getScanFrom()) || isa<Zeroes>(st->getScanThru()))) {
349            return st->getScanFrom();
350        } else if (LLVM_UNLIKELY(isa<Ones>(st->getScanThru()))) {
351            block->setInsertPoint(stmt->getPrevNode());
352            return block->createZeroes(stmt->getType());
353        } else if (LLVM_UNLIKELY(isa<ScanThru>(st->getScanFrom()))) {
354            ScanThru * const nested = cast<ScanThru>(st->getScanFrom());
355            if (LLVM_UNLIKELY(st->getScanThru() == nested->getScanThru())) {
356                return nested;
357            }
358        }
359    } else if (isa<MatchStar>(stmt)) {
360        MatchStar * const mstar = cast<MatchStar>(stmt);
361        if (LLVM_UNLIKELY(isa<Zeroes>(mstar->getMarker()) || isa<Zeroes>(mstar->getCharClass()))) {
362            return mstar->getMarker();
363        } else if (LLVM_UNLIKELY(isa<Ones>(mstar->getMarker()))) {
364            block->setInsertPoint(stmt->getPrevNode());
365            return block->createOnes(stmt->getType());
366        }
367    } else if (isa<Lookahead>(stmt)) {
368        Lookahead * const la = cast<Lookahead>(stmt);
369        if (LLVM_UNLIKELY(isa<Zeroes>(la->getExpression()) || la->getAmount() == 0)) {
370            return la->getExpression();
371        }
372    } else if (LLVM_UNLIKELY(isa<Sel>(stmt))) {
373        Sel * const sel = cast<Sel>(stmt);
374        if (LLVM_UNLIKELY(isa<Zeroes>(sel->getCondition()))) {
375            return sel->getFalseExpr();
376        }
377        if (LLVM_UNLIKELY(isa<Ones>(sel->getCondition()))) {
378            return sel->getTrueExpr();
379        }
380        if (LLVM_UNLIKELY(isa<Zeroes>(sel->getTrueExpr()))) {
381            block->setInsertPoint(stmt->getPrevNode());
382            PabloAST * const negCond = triviallyFold(block->createNot(sel->getCondition()), block);
383            return triviallyFold(block->createAnd(sel->getFalseExpr(), negCond), block);
384        }
385        if (LLVM_UNLIKELY(isa<Ones>(sel->getTrueExpr()))) {
386            block->setInsertPoint(stmt->getPrevNode());
387            return triviallyFold(block->createOr(sel->getCondition(), sel->getFalseExpr()), block);
388        }
389        if (LLVM_UNLIKELY(isa<Zeroes>(sel->getFalseExpr()))) {
390            block->setInsertPoint(stmt->getPrevNode());
391            return triviallyFold(block->createAnd(sel->getCondition(), sel->getTrueExpr()), block);
392        }
393        if (LLVM_UNLIKELY(isa<Ones>(sel->getFalseExpr()))) {
394            block->setInsertPoint(stmt->getPrevNode());
395            PabloAST * const negCond = triviallyFold(block->createNot(sel->getCondition()), block);
396            return triviallyFold(block->createOr(sel->getTrueExpr(), negCond), block);
397        }
398    } else if (LLVM_UNLIKELY(isa<Add>(stmt) || isa<Subtract>(stmt))) {
399       if (LLVM_UNLIKELY(isa<Integer>(stmt->getOperand(0)) && isa<Integer>(stmt->getOperand(1)))) {
400           const Integer * const int0 = cast<Integer>(stmt->getOperand(0));
401           const Integer * const int1 = cast<Integer>(stmt->getOperand(1));
402           Integer::IntTy result = 0;
403           if (isa<Add>(stmt)) {
404               result = int0->value() + int1->value();
405           } else {
406               result = int0->value() - int1->value();
407           }
408           return block->getInteger(result);
409       }
410    }
411    return nullptr;
412}
413
414/** ------------------------------------------------------------------------------------------------------------- *
415 * @brief isTrivial
416 *
417 * If this inner block is composed of only Boolean logic and Assign statements and there are fewer than 3
418 * statements, just add the statements in the inner block to the current block
419 ** ------------------------------------------------------------------------------------------------------------- */
420static bool isTrivial(const PabloBlock * const block) {
421    unsigned computations = 0;
422    for (const Statement * stmt : *block) {
423        switch (stmt->getClassTypeId()) {
424            case TypeId::And:
425            case TypeId::Or:
426            case TypeId::Xor:
427                if (++computations > 3) {
428                    return false;
429                }
430            case TypeId::Not:
431            case TypeId::Assign:
432                break;
433            default:
434                return false;
435        }
436    }
437    return true;
438}
439
440/** ------------------------------------------------------------------------------------------------------------- *
441 * @brief flatten
442 ** ------------------------------------------------------------------------------------------------------------- */
443static Statement * flatten(Branch * const br) {
444    Statement * stmt = br;
445    Statement * nested = br->getBody()->front();
446    while (nested) {
447        Statement * next = nested->removeFromParent();
448        nested->insertAfter(stmt);
449        stmt = nested;
450        nested = next;
451    }
452    return br->eraseFromParent();
453}
454
455/** ------------------------------------------------------------------------------------------------------------- *
456 * @brief isNonZero
457 ** ------------------------------------------------------------------------------------------------------------- */
458bool isNonZero(const PabloAST * const expr) const {
459    return isa<Ones>(expr) || std::find(mNonZero.begin(), mNonZero.end(), expr) != mNonZero.end();
460}
461
462/** ------------------------------------------------------------------------------------------------------------- *
463 * @brief strengthReduction
464 *
465 * Find and replace any Pablo operations with a less expensive equivalent operation whenever possible.
466 ** ------------------------------------------------------------------------------------------------------------- */
467void strengthReduction(PabloBlock * const block) {
468
469    Statement * stmt = block->front();
470    while (stmt) {
471        if (isa<Branch>(stmt)) {
472            strengthReduction(cast<Branch>(stmt)->getBody());
473        } else if (isa<Advance>(stmt)) {
474            Advance * adv = cast<Advance>(stmt);
475            if (LLVM_UNLIKELY(isa<Advance>(adv->getOperand(0)))) {
476                // Replace an Advance(Advance(x, n), m) with an Advance(x,n + m)
477                // Test whether this will generate a long advance and abort?
478                Advance * op = cast<Advance>(stmt->getOperand(0));
479                if (LLVM_UNLIKELY(op->getNumUses() == 1)) {
480                    adv->setOperand(0, op->getOperand(0));
481                    adv->setOperand(1, block->getInteger(adv->getAmount() + op->getAmount()));
482                    op->eraseFromParent(false);
483                }
484            }
485        } else if (LLVM_UNLIKELY(isa<ScanThru>(stmt))) {           
486            ScanThru * const outer = cast<ScanThru>(stmt);
487            if (LLVM_UNLIKELY(isa<Advance>(outer->getScanFrom()))) {
488                // Replace ScanThru(Advance(x,n),y) with ScanThru(Advance(x, n - 1), Advance(x, n - 1) | y), where Advance(x, 0) = x               
489                Advance * const inner = cast<Advance>(outer->getScanFrom());
490                if (LLVM_UNLIKELY(inner->getNumUses() == 1)) {
491                    PabloAST * stream = inner->getExpression();
492                    block->setInsertPoint(stmt);
493                    if (LLVM_UNLIKELY(inner->getAmount() != 1)) {
494                        stream = block->createAdvance(stream, block->getInteger(inner->getAmount() - 1));
495                    }
496                    stmt = outer->replaceWith(block->createAdvanceThenScanThru(stream, outer->getScanThru()));
497                    inner->eraseFromParent(false);
498                    continue;
499                }
500//            } else if (LLVM_UNLIKELY(isa<ScanThru>(outer->getScanFrom()))) {
501//                // Replace ScanThru(ScanThru(x, y), z) with ScanThru(x, y | z)
502//                // TODO: this transformation is valid if and only if there can be no instance of ...yzy... in the (y | z) stream
503//                // but that degree of reasoning is too complex to perform linearly here
504//                ScanThru * const inner = cast<ScanThru>(outer->getScanFrom());
505//                block->setInsertPoint(stmt);
506//                ScanThru * const scanThru = block->createScanThru(inner->getScanFrom(), block->createOr(inner->getScanThru(), outer->getScanThru()));
507//                stmt->replaceWith(scanThru);
508//                stmt = scanThru;
509//                continue;
510            } else if (LLVM_UNLIKELY(isa<And>(outer->getScanFrom()))) {
511                // Suppose B is an arbitrary bitstream and A = Advance(B, 1). ScanThru(B ∧ ¬A, B) will leave a marker on the position
512                // following the end of any run of 1-bits in B. But this is equivalent to computing A ∧ ¬B since A will have exactly
513                // one 1-bit past the end of any run of 1-bits in B.
514
515
516
517
518
519            }
520        } else if (LLVM_UNLIKELY(isa<ScanTo>(stmt))) {
521            ScanTo * scanTo = cast<ScanTo>(stmt);
522            if (LLVM_UNLIKELY(isa<Advance>(scanTo->getScanFrom()))) {
523                // Replace a ScanTo(Advance(x,n),y) with an ScanTo(Advance(x, n - 1), Advance(x, n - 1) | y), where Advance(x, 0) = x
524                Advance * adv = cast<Advance>(scanTo->getScanFrom());
525                if (LLVM_UNLIKELY(adv->getNumUses() == 1)) {
526                    PabloAST * stream = adv->getExpression();
527                    block->setInsertPoint(stmt);
528                    if (LLVM_UNLIKELY(adv->getAmount() != 1)) {
529                        stream = block->createAdvance(stream, block->getInteger(adv->getAmount() - 1));
530                    }
531                    stmt = scanTo->replaceWith(block->createAdvanceThenScanTo(stream, scanTo->getScanTo()));
532                    adv->eraseFromParent(false);
533                    continue;
534                }
535            }
536        }
537        stmt = stmt->getNextNode();
538    }
539}
540
541/** ------------------------------------------------------------------------------------------------------------- *
542 * @brief deadCodeElimination
543 ** ------------------------------------------------------------------------------------------------------------- */
544void deadCodeElimination(PabloBlock * const block) {
545
546    flat_set<PabloAST *> written;
547
548    for (Statement * stmt = block->back(), * prior; stmt; stmt = prior) {
549        prior = stmt->getPrevNode();
550        if (LLVM_UNLIKELY(stmt->getNumUses() == 0)) {
551            if (LLVM_UNLIKELY(isa<Branch>(stmt))) {
552                written.clear();
553                deadCodeElimination(cast<Branch>(stmt)->getBody());
554            } else if (LLVM_UNLIKELY(isa<Assign>(stmt))) {
555                // An Assign statement is locally dead whenever its variable is not read
556                // before being reassigned a value.
557                PabloAST * var = cast<Assign>(stmt)->getVariable();
558                if (LLVM_UNLIKELY(!written.insert(var).second)) {
559                    stmt->eraseFromParent();
560                }
561            } else {
562                stmt->eraseFromParent();
563            }
564        }
565    }
566}
567
568std::vector<const PabloAST *>       mNonZero;
569std::vector<const PabloBlock *>     mInScope;
570
571};
572
573/** ------------------------------------------------------------------------------------------------------------- *
574 * @brief optimize
575 ** ------------------------------------------------------------------------------------------------------------- */
576bool Simplifier::optimize(PabloKernel * kernel) {
577    PassContainer pc;
578    pc.run(kernel);
579    #ifndef NDEBUG
580    PabloVerifier::verify(kernel, "post-simplification");
581    #endif
582    return true;
583}
584
585}
Note: See TracBrowser for help on using the repository browser.