source: icGREP/icgrep-devel/icgrep/pablo/optimizers/booleanreassociationpass.cpp @ 4754

Last change on this file since 4754 was 4754, checked in by nmedfort, 4 years ago

Start of work on applying the distribution law to the AST.

File size: 27.8 KB
Line 
1#include "booleanreassociationpass.h"
2#include <boost/container/flat_set.hpp>
3#include <boost/container/flat_map.hpp>
4#include <boost/circular_buffer.hpp>
5#include <boost/graph/adjacency_list.hpp>
6#include <boost/graph/topological_sort.hpp>
7#include <pablo/optimizers/pablo_simplifier.hpp>
8#include <queue>
9#include <iostream>
10#include <pablo/printer_pablos.h>
11
12
13using namespace boost;
14using namespace boost::container;
15
16namespace pablo {
17
18using Graph = BooleanReassociationPass::Graph;
19using Vertex = Graph::vertex_descriptor;
20using VertexQueue = circular_buffer<Vertex>;
21using Map = std::unordered_map<PabloAST *, Vertex>;
22using EdgeQueue = std::queue<std::pair<Vertex, Vertex>>;
23
24static void redistributeAST(PabloBlock &block, Graph & G);
25
26/** ------------------------------------------------------------------------------------------------------------- *
27 * @brief optimize
28 ** ------------------------------------------------------------------------------------------------------------- */
29bool BooleanReassociationPass::optimize(PabloFunction & function) {
30    BooleanReassociationPass brp;
31    brp.resolveScopes(function);
32    brp.processScopes(function);
33    Simplifier::optimize(function);
34    return true;
35}
36
37/** ------------------------------------------------------------------------------------------------------------- *
38 * @brief resolveScopes
39 ** ------------------------------------------------------------------------------------------------------------- */
40inline void BooleanReassociationPass::resolveScopes(PabloFunction &function) {
41    resolveScopes(function.getEntryBlock());
42}
43
44/** ------------------------------------------------------------------------------------------------------------- *
45 * @brief resolveScopes
46 ** ------------------------------------------------------------------------------------------------------------- */
47void BooleanReassociationPass::resolveScopes(PabloBlock & block) {
48    for (Statement * stmt : block) {
49        if (isa<If>(stmt) || isa<While>(stmt)) {
50            PabloBlock & nested = isa<If>(stmt) ? cast<If>(stmt)->getBody() : cast<While>(stmt)->getBody();
51            mResolvedScopes.emplace(&nested, stmt);
52            resolveScopes(nested);
53        }
54    }
55}
56
57
58/** ------------------------------------------------------------------------------------------------------------- *
59 * @brief scan
60 ** ------------------------------------------------------------------------------------------------------------- */
61inline void BooleanReassociationPass::processScopes(PabloFunction & function) {
62    std::vector<Statement *> terminals;
63    for (unsigned i = 0; i != function.getNumOfResults(); ++i) {
64        terminals.push_back(function.getResult(i));
65    }
66    processScopes(function.getEntryBlock(), std::move(terminals));
67}
68
69/** ------------------------------------------------------------------------------------------------------------- *
70 * @brief scan
71 ** ------------------------------------------------------------------------------------------------------------- */
72void BooleanReassociationPass::processScopes(PabloBlock & block, std::vector<Statement *> && terminals) {
73    processScope(block, std::move(terminals));
74    for (Statement * stmt : block) {
75        if (isa<If>(stmt)) {
76            const auto & defs = cast<const If>(stmt)->getDefined();
77            std::vector<Statement *> terminals(defs.begin(), defs.end());
78            processScopes(cast<If>(stmt)->getBody(), std::move(terminals));
79        } else if (isa<While>(stmt)) {
80            const auto & vars = cast<const While>(stmt)->getVariants();
81            std::vector<Statement *> terminals(vars.begin(), vars.end());
82            processScopes(cast<While>(stmt)->getBody(), std::move(terminals));
83        }
84    }
85}
86
87/** ------------------------------------------------------------------------------------------------------------- *
88 * @brief is_power_of_2
89 * @param n an integer
90 ** ------------------------------------------------------------------------------------------------------------- */
91static inline bool is_power_of_2(const size_t n) {
92    return ((n & (n - 1)) == 0);
93}
94
95/** ------------------------------------------------------------------------------------------------------------- *
96 * @brief log2_plus_one
97 ** ------------------------------------------------------------------------------------------------------------- */
98static inline size_t ceil_log2(const size_t n) {
99    return std::log2<size_t>(n) + (is_power_of_2(n) ? 0 : 1);
100}
101
102/** ------------------------------------------------------------------------------------------------------------- *
103 * @brief isOptimizable
104 *
105 * And, Or and Xor instructions are all associative, commutative and distributive operations. Thus we can
106 * safely rearrange expressions such as "((((a √ b) √ c) √ d) √ e) √ f" into "((a √ b) √ (c √ d)) √ (e √ f)".
107 ** ------------------------------------------------------------------------------------------------------------- */
108static inline bool isOptimizable(const PabloAST * const expr) {
109    assert (expr);
110    switch (expr->getClassTypeId()) {
111        case PabloAST::ClassTypeId::And:
112        case PabloAST::ClassTypeId::Or:
113        case PabloAST::ClassTypeId::Xor:
114            return true;
115        default:
116            return false;
117    }
118}
119
120/** ------------------------------------------------------------------------------------------------------------- *
121 * @brief inCurrentBlock
122 ** ------------------------------------------------------------------------------------------------------------- */
123static inline bool inCurrentBlock(const Statement * stmt, const PabloBlock & block) {
124    return stmt->getParent() == &block;
125}
126
127static inline bool inCurrentBlock(const PabloAST * expr, const PabloBlock & block) {
128    return isa<Statement>(expr) && inCurrentBlock(cast<Statement>(expr), block);
129}
130
131/** ------------------------------------------------------------------------------------------------------------- *
132 * @brief isCutNecessary
133 ** ------------------------------------------------------------------------------------------------------------- */
134static inline bool isCutNecessary(const Vertex u, const Vertex v, const Graph & G, const std::vector<unsigned> & component) {
135    // Either this edge crosses a component boundary or the operations performed by the vertices differs, we need to cut
136    // the graph here and generate two partial equations.
137    if (LLVM_UNLIKELY(component[u] != component[v])) {
138        return true;
139    } else if (LLVM_UNLIKELY((in_degree(u, G) != 0) && (G[u]->getClassTypeId() != G[v]->getClassTypeId()))) {
140        return true;
141    }
142    return false;
143}
144
145/** ------------------------------------------------------------------------------------------------------------- *
146 * @brief push
147 ** ------------------------------------------------------------------------------------------------------------- */
148static inline void push(const Vertex u, VertexQueue & Q) {
149    if (LLVM_UNLIKELY(Q.full())) {
150        Q.set_capacity(Q.capacity() * 2);
151    }
152    Q.push_back(u);
153    assert (Q.back() == u);
154}
155
156/** ------------------------------------------------------------------------------------------------------------- *
157 * @brief pop
158 ** ------------------------------------------------------------------------------------------------------------- */
159static inline Vertex pop(VertexQueue & Q) {
160    assert (!Q.empty() && "Popping an empty vertex queue");
161    const Vertex u = Q.front();
162    Q.pop_front();
163    return u;
164}
165
166/** ------------------------------------------------------------------------------------------------------------- *
167 * @brief getVertex
168 ** ------------------------------------------------------------------------------------------------------------- */
169static inline Vertex getVertex(PabloAST * expr, Graph & G, Map & M) {
170    const auto f = M.find(expr);
171    if (f != M.end()) {
172        return f->second;
173    }
174    const auto u = add_vertex(expr, G);
175    M.insert(std::make_pair(expr, u));
176    return u;
177}
178
179/** ------------------------------------------------------------------------------------------------------------- *
180 * @brief createTree
181 ** ------------------------------------------------------------------------------------------------------------- */
182static PabloAST * createTree(PabloBlock & block, const PabloAST::ClassTypeId typeId, circular_buffer<PabloAST *> & Q) {
183    while (Q.size() > 1) {
184        PabloAST * e1 = Q.front(); Q.pop_front();
185        PabloAST * e2 = Q.front(); Q.pop_front();
186        PabloAST * expr = nullptr;
187        switch (typeId) {
188            case PabloAST::ClassTypeId::And:
189                expr = block.createAnd(e1, e2); break;
190            case PabloAST::ClassTypeId::Or:
191                expr = block.createOr(e1, e2); break;
192            case PabloAST::ClassTypeId::Xor:
193                expr = block.createXor(e1, e2); break;
194            default: break;
195        }
196        Q.push_back(expr);
197    }
198    PabloAST * r = Q.front();
199    Q.clear();
200    return r;
201}
202
203/** ------------------------------------------------------------------------------------------------------------- *
204 * @brief printGraph
205 ** ------------------------------------------------------------------------------------------------------------- */
206static void printGraph(PabloBlock & block, const Graph & G) {
207    raw_os_ostream out(std::cerr);
208    out << "digraph G {\n";
209    for (auto u : make_iterator_range(vertices(G))) {
210        out << "v" << u << " [label=\"";
211        PabloAST * expr = G[u];
212        if (isa<Statement>(expr)) {
213            if (LLVM_UNLIKELY(isa<If>(expr))) {
214                out << "If ";
215                PabloPrinter::print(cast<If>(expr)->getOperand(0), out);
216                out << ":";
217            } else if (LLVM_UNLIKELY(isa<While>(expr))) {
218                out << "While ";
219                PabloPrinter::print(cast<While>(expr)->getOperand(0), out);
220                out << ":";
221            } else {
222                PabloPrinter::print(cast<Statement>(expr), "", out);
223            }
224        } else {
225            PabloPrinter::print(expr, out);
226        }
227        out << "\"";
228        if (!inCurrentBlock(expr, block)) {
229            out << " style=dashed";
230        }
231        out << "];\n";
232    }
233    for (auto e : make_iterator_range(edges(G))) {
234        out << "v" << source(e, G) << " -> v" << target(e, G) << ";\n";
235    }
236
237    out << "{ rank=same;";
238    for (auto u : make_iterator_range(vertices(G))) {
239        if (in_degree(u, G) == 0 && out_degree(u, G) != 0) {
240            out << " v" << u;
241        }
242    }
243    out << "}\n";
244
245    out << "{ rank=same;";
246    for (auto u : make_iterator_range(vertices(G))) {
247        if (out_degree(u, G) == 0 && in_degree(u, G) != 0) {
248            out << " v" << u;
249        }
250    }
251    out << "}\n";
252
253    out << "}\n\n";
254    out.flush();
255}
256
257/** ------------------------------------------------------------------------------------------------------------- *
258 * @brief processScope
259 ** ------------------------------------------------------------------------------------------------------------- */
260void BooleanReassociationPass::processScope(PabloBlock & block, std::vector<Statement *> && terminals) {
261
262    Graph G;
263    summarizeAST(block, std::move(terminals), G);
264    redistributeAST(block, G);
265
266
267
268
269    printGraph(block, G);
270
271}
272
273/** ------------------------------------------------------------------------------------------------------------- *
274 * @brief summarizeAST
275 *
276 * This function scans through a basic block (starting by its terminals) and computes a DAG in which any sequences
277 * of AND, OR or XOR functions are "flattened" and allowed to have any number of inputs. This allows us to
278 * reassociate them in the most efficient way possible.
279 ** ------------------------------------------------------------------------------------------------------------- */
280void BooleanReassociationPass::summarizeAST(PabloBlock & block, std::vector<Statement *> && terminals, Graph & G) {
281
282    Map M;
283    VertexQueue Q(128);
284    EdgeQueue E;
285
286    for (;;) {
287
288        Graph Gk;
289        Map Mk;
290
291        // Generate a graph depicting the relationships between the terminals. If the original terminals
292        // cannot be optimized with this algorithm bypass them in favour of their operands. If those cannot
293        // be optimized, they'll be left as the initial terminals for the next "layer" of the AST.
294
295        for (Statement * term : terminals) {
296            if (LLVM_LIKELY(Mk.count(term) == 0)) {
297                // add or find this terminal in our global graph
298                Vertex x = getVertex(term, G, M);
299                if (inCurrentBlock(term, block)) {
300                    if (isOptimizable(term)) {
301                        const Vertex u = add_vertex(term, Gk);
302                        Mk.insert(std::make_pair(term, u));
303                        push(u, Q);
304                        continue;
305                    }
306                } else if (isa<Assign>(term) || isa<Next>(term)) {
307                    // If this is an Assign (Next) node whose operand does not originate from the current block
308                    // then check to see if there is an If (While) node that does.
309                    Statement * branch = nullptr;
310                    if (isa<Assign>(term)) {
311                        for (PabloAST * user : term->users()) {
312                            if (isa<If>(user)) {
313                                const If * ifNode = cast<If>(user);
314                                if (inCurrentBlock(ifNode, block)) {
315                                    const auto & defs = ifNode->getDefined();
316                                    if (LLVM_LIKELY(std::find(defs.begin(), defs.end(), cast<Assign>(term)) != defs.end())) {
317                                        branch = cast<Statement>(user);
318                                        break;
319                                    }
320                                }
321                            }
322                        }
323                    } else { // if (isa<Next>(term))
324                        for (PabloAST * user : term->users()) {
325                            if (isa<While>(user)) {
326                                const While * whileNode = cast<While>(user);
327                                if (inCurrentBlock(whileNode, block)) {
328                                    const auto & vars = whileNode->getVariants();
329                                    if (LLVM_LIKELY(std::find(vars.begin(), vars.end(), cast<Next>(term)) != vars.end())) {
330                                        branch = cast<Statement>(user);
331                                        break;
332                                    }
333                                }
334                            }
335                        }
336                    }
337
338                    // If we didn't find a branch, then the Assign (Next) node must have come from a preceeding
339                    // block. Just skip it for now.
340                    if (branch == nullptr) {
341                        continue;
342                    }
343
344                    // Otherwise add the branch to G and test its operands rather than the original terminal
345                    const Vertex z = getVertex(branch, G, M);
346                    add_edge(z, x, G);
347                    x = z;
348                    term = branch;
349                }
350
351                for (unsigned i = 0; i != term->getNumOperands(); ++i) {
352                    PabloAST * const op = term->getOperand(i);
353                    if (LLVM_LIKELY(inCurrentBlock(op, block))) {
354                        const Vertex y = getVertex(op, G, M);
355                        add_edge(y, x, G);
356                        if (LLVM_LIKELY(Mk.count(op) == 0)) {
357                            const Vertex v = add_vertex(op, Gk);
358                            Mk.insert(std::make_pair(op, v));
359                            push(v, Q);
360                        }
361                    }
362                }
363            }
364        }
365
366        if (LLVM_UNLIKELY(Q.empty())) {
367            break;
368        }
369
370        for (;;) {
371            const Vertex u = pop(Q);
372            if (isOptimizable(Gk[u])) {
373                Statement * stmt = cast<Statement>(Gk[u]);
374                // If any user of this statement is not in the current block, determine the outermost If/While node
375                // that contains this statement within and add an edge from this statement to it to denote both the
376                // topological ordering necessary and that this statement must be computed.
377                for (PabloAST * user : stmt->users()) {
378                    if (LLVM_LIKELY(isa<Statement>(user))) {
379                        PabloBlock * parent = cast<Statement>(user)->getParent();
380                        if (LLVM_UNLIKELY(parent != &block)) {
381                            while (parent->getParent() != &block) {
382                                parent = parent->getParent();
383                            }
384                            if (LLVM_UNLIKELY(parent == nullptr)) {
385                                throw std::runtime_error("Could not locate nested scope block!");
386                            }
387                            const auto f = mResolvedScopes.find(parent);
388                            if (LLVM_UNLIKELY(f == mResolvedScopes.end())) {
389                                throw std::runtime_error("Failed to resolve scope block!");
390                            }
391                            add_edge(u, getVertex(f->second, Gk, Mk), Gk);
392                        }
393                    }
394                }
395                // Scan through the use-def chains to locate any chains of rearrangable expressions and their inputs
396                for (unsigned i = 0; i != 2; ++i) {
397                    PabloAST * op = stmt->getOperand(i);
398                    auto f = Mk.find(op);
399                    if (f == Mk.end()) {
400                        const Vertex v = add_vertex(op, Gk);
401                        f = Mk.insert(std::make_pair(op, v)).first;
402                        if (op->getClassTypeId() == stmt->getClassTypeId() && inCurrentBlock(cast<Statement>(op), block)) {
403                            push(v, Q);
404                        }
405                    }
406                    add_edge(f->second, u, Gk);
407                }
408            }
409            if (Q.empty()) {
410                break;
411            }
412        }
413
414        // Generate a topological ordering for G; if one of our terminals happens to also be a partial computation of
415        // another terminal, we need to make sure we compute it as an independent subexpression.
416        std::vector<unsigned> ordering;
417        ordering.reserve(num_vertices(Gk));
418        topological_sort(Gk, std::back_inserter(ordering));
419        std::vector<unsigned> component(num_vertices(Gk));
420
421        for (;;) {
422
423            // Mark which computation component these vertices are in based on their topological (occurence) order.
424            unsigned components = 0;
425            for (auto u : ordering) {
426                unsigned id = 0;
427                // If this is a sink in G, it is the root of a new component.
428                if (out_degree(u, Gk) == 0) {
429                    id = ++components;
430                } else { // otherwise it belongs to the outermost component.
431                    for (auto e : make_iterator_range(out_edges(u, Gk))) {
432                        id = std::max(id, component[target(e, Gk)]);
433                    }
434                }
435                assert (id && "Topological ordering failed!");
436                component[u] = id;
437            }
438
439            // Cut the graph wherever a computation crosses a component or whenever we need to cut the graph because
440            // the instructions corresponding to the pair of nodes differs.
441            graph_traits<Graph>::edge_iterator ei, ei_end;
442            for (std::tie(ei, ei_end) = edges(Gk); ei != ei_end; ) {
443                const Graph::edge_descriptor e = *ei++;
444                const Vertex u = source(e, Gk);
445                const Vertex v = target(e, Gk);
446                if (LLVM_UNLIKELY(isCutNecessary(u, v, Gk, component))) {
447                    E.push(std::make_pair(u, v));
448                    remove_edge(u, v, Gk);
449                }
450            }
451
452            // If no cuts are necessary, we're done.
453            if (E.empty()) {
454                break;
455            }
456
457            for (;;) {
458
459                Vertex u, v;
460                std::tie(u, v) = E.front(); E.pop();
461
462                // The vertex belonging to a component with a greater number must come "earlier"
463                // in the program. By replicating it, this ensures it's computed as an output of
464                // one component and used as an input of another.
465                if (component[u] < component[v]) {
466                    std::swap(u, v);
467                }
468
469                // Replicate u and fix the ordering and component vectors to reflect the change in Gk.
470                Vertex w = add_vertex(Gk[u], Gk);
471                ordering.insert(std::find(ordering.begin(), ordering.end(), u), w);
472                assert (component.size() == w);
473                component.push_back(component[v]);
474                add_edge(w, v, Gk);
475
476                // However, after we do so, we need to make sure the original source vertex will be a
477                // sink in Gk unless it is also an input variable (in which case we'd simply end up with
478                // extraneous isolated vertex. Otherwise, we need to make further cuts and replications.
479                if (in_degree(u, Gk) != 0) {
480                    for (auto e : make_iterator_range(out_edges(u, Gk))) {
481                        E.push(std::make_pair(source(e, Gk), target(e, Gk)));
482                    }
483                    clear_out_edges(u, Gk);
484                }
485
486                if (E.empty()) {
487                    break;
488                }
489            }
490        }
491
492        // Scan through the graph so that we process the outermost expressions first
493        for (const Vertex u : ordering) {
494            if (LLVM_UNLIKELY(out_degree(u, Gk) == 0)) {
495                // Create a vertex marking the output statement we may end up replacing
496                // and collect the set of source variables in the component
497                const Vertex x = getVertex(Gk[u], G, M);
498                if (LLVM_LIKELY(in_degree(u, Gk) > 0)) {
499                    flat_set<PabloAST *> vars;
500                    flat_set<Vertex> visited;
501                    for (Vertex v = u;;) {
502                        if (in_degree(v, Gk) == 0) {
503                            vars.insert(Gk[v]);
504                        } else {
505                            for (auto e : make_iterator_range(in_edges(v, Gk))) {
506                                const Vertex w = source(e, Gk);
507                                if (LLVM_LIKELY(visited.insert(w).second)) {
508                                    push(w, Q);
509                                }
510                            }
511                        }
512                        if (Q.empty()) {
513                            break;
514                        }
515                        v = pop(Q);
516                    }
517                    for (PabloAST * var : vars) {
518                        add_edge(getVertex(var, G, M), x, G);
519                    }
520                }
521            }
522        }
523
524        // Determine the source variables of the next "layer" of the AST
525        flat_set<Statement *> nextSet;
526        for (auto u : ordering) {
527            if (LLVM_UNLIKELY(in_degree(u, Gk) == 0 && isa<Statement>(Gk[u]))) {
528                nextSet.insert(cast<Statement>(Gk[u]));
529            } else if (LLVM_UNLIKELY(out_degree(u, Gk) == 0 && isa<Statement>(Gk[u]))) {
530                // some input will also be the output of some subgraph of Gk whenever we cut and
531                // replicated a vertex. We don't need to reevaluate it as part of the next layer.
532                nextSet.erase(cast<Statement>(Gk[u]));
533            }
534        }
535
536        if (LLVM_UNLIKELY(nextSet.empty())) {
537            break;
538        }
539
540        terminals.assign(nextSet.begin(), nextSet.end());
541    }
542}
543
544/** ------------------------------------------------------------------------------------------------------------- *
545 * @brief redistributeAST
546 *
547 * Apply the distribution law to reduce computations whenever possible.
548 ** ------------------------------------------------------------------------------------------------------------- */
549static void redistributeAST(PabloBlock & block, Graph & G) {
550    using TypeId = PabloAST::ClassTypeId;
551
552    // Process the graph in reverse topological order so that we end up recursively applying the distribution law
553    // to the entire AST.
554    std::vector<unsigned> ordering;
555    ordering.reserve(num_vertices(G));
556    topological_sort(G, std::back_inserter(ordering));
557
558    for (const Vertex u : ordering) {
559        const TypeId outerTypeId = G[u]->getClassTypeId();
560        if (outerTypeId == TypeId::And || outerTypeId == TypeId::Or) {
561            if (inCurrentBlock(cast<Statement>(G[u]), block)) {
562                const TypeId innerTypeId = (outerTypeId == TypeId::And) ? TypeId::Or : TypeId::And;
563
564                Graph H;
565                Map M;
566
567                flat_set<Vertex> distributable;
568
569                for (auto e : make_iterator_range(in_edges(u, G))) {
570                    const Vertex v = source(e, G);
571                    if (G[v]->getClassTypeId() == innerTypeId && inCurrentBlock(cast<Statement>(G[v]), block)) {
572                        bool safe = true;
573                        // This operation is safe to transform if all of its users are AND or OR instructions.
574                        if (out_degree(v, G) > 1) {
575                            for (auto e : make_iterator_range(out_edges(v, G))) {
576                                const PabloAST * expr = G[target(e, G)];
577                                if (expr->getClassTypeId() != TypeId::And && expr->getClassTypeId() != TypeId::Or) {
578                                    safe = false;
579                                    break;
580                                }
581                            }
582                        }
583                        if (safe) {
584                            distributable.insert(v);
585                        }
586                    }
587                }
588
589                if (distributable.size() > 1) {
590                    flat_map<Vertex, unsigned> sources;
591                    bool canRedistribute = false;
592                    for (const Vertex v : distributable) {
593                        for (auto e : make_iterator_range(in_edges(v, G))) {
594                            const Vertex w = source(e, G);
595                            const auto f = sources.find(w);
596                            if (f == sources.end()) {
597                                sources.emplace(w, 1);
598                            } else {
599                                f->second += 1;
600                                canRedistribute = true;
601                            }
602                        }
603                    }
604                    if (canRedistribute) {
605                        Vertex w = 0;
606                        unsigned count = 0;
607                        const Vertex x = getVertex(G[u], H, M);
608                        for (auto s : sources) {
609                            std::tie(w, count) = s;
610                            if (count > 1) {
611                                const Vertex z = getVertex(G[w], H, M);
612                                for (auto e : make_iterator_range(out_edges(w, G))) {
613                                    const Vertex v = target(e, G);
614                                    if (distributable.count(v)) {
615                                        const Vertex y = getVertex(G[v], H, M);
616                                        add_edge(y, x, H);
617                                        add_edge(z, y, H);
618                                    }
619                                }
620                            }
621                        }
622                    }
623                }
624
625                printGraph(block, H);
626            }
627        }
628    }
629
630
631
632
633
634}
635
636/** ------------------------------------------------------------------------------------------------------------- *
637 * @brief applyDistributionLaw
638 ** ------------------------------------------------------------------------------------------------------------- */
639BooleanReassociationPass::BooleanReassociationPass() { }
640
641
642}
Note: See TracBrowser for help on using the repository browser.