# FlowChart.java

    /*
    ** Module   : FlowChart.java
    ** Abstract : Computes the flow chart associated with a top-level block.
    **
    ** Copyright (c) 2018-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description----------------------------------
    ** 001 CA  20181003 First version.
    ** 002 CA  20200428 Some changes to process DEFINE ENUM.  Untested.
    ** 003 TJD 20220504 Java 11 compatibility minor changes
    ** 004 OM  20230115 Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
    **                  versions, based on node types rather on string paths.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.report.server;

    import java.util.*;
    import java.util.concurrent.atomic.*;
    import java.util.function.*;

    import org.apache.tinkerpop.gremlin.structure.Direction;
    import org.apache.tinkerpop.gremlin.structure.Vertex;

    import com.goldencode.ast.*;
    import com.goldencode.p2j.uast.*;

    /**
     * Process a top-level block and compute the associated flow chart.
     * <p>
     * Each block will have only one START node; for nested blocks, there will be only 1 END node;
     * for the top-level block there will be 1 or more EXIT nodes (if the EXIT is performed from a
     * deeply nested block, then a new EXIT node will be added at that level, to avoid making the
     * top-level block's EXIT node a super-node).
     * <p>
     * Each block is processed recursively, and all direct child nodes of this block will have as
     * group ID the block's ID.
     * <p>
     * When branching, it is important to specify for each branch's statement the ID of the first
     * node in the branch - as the branch nodes will start from the same depth, this will allow the
     * nodes to be ordered by their parent ID and placed on the same parent's side.
     * <p>
     * Loop blocks with WHILE and/or TO expressions will have the expression expanded, to mimic the
     * execution of the loop: for WHILE, there will be a 'backlink' to the WHILE condition, and for
     * TO expression, there will be additional assignment and loop incrementation nodes added - see
     * {@link #loadBlockFlow}'s comments for more details.
     */
    public class FlowChart
    implements ProgressParserTokenTypes
    {
       /** Map labels with their associated blocks. */
       private final Map<String, Aast> labelTargets = new HashMap<>();

       /** Maps AST IDs to their {@link FlowAstNode node} counterpart. */
       private final Map<Long, FlowAstNode> astsToNodes = new HashMap<>();

       /** For each node, map the block's ID to its associated START node. */
       private final Map<Integer, FlowNode> startNodes = new HashMap<>();

       /** Flag identifying the flow chart direction - when <code>true</code>, vertical flow. */
       private final boolean flowDir;

       /** The created nodes. */
       private List<FlowNode> nodes;

       /** The created links. */
       private List<FlowLink> links;

       /** A set of statement types explicitly included in the chart, which are not INNER_BLOCK. */
       private Set<Integer> statementTypes = new HashSet<>(Arrays.asList(KW_IF,
                                                                         KW_CASE,
                                                                         KW_LEAVE,
                                                                         KW_NEXT,
                                                                         KW_RETRY,
                                                                         KW_RETURN,
                                                                         KW_UNDO,
                                                                         KW_STOP,
                                                                         KW_QUIT,
                                                                         KW_CATCH,
                                                                         KW_FINALLY));

       /** The set of block types. */
       private Set<Integer> blockTypes = new HashSet<>(Arrays.asList(KW_DO, KW_REPEAT, KW_FOR));

       /** Used when branching: track the creation order of each same-depth node. */
       private Map<Integer, AtomicInteger> depthOrder = new HashMap<>();

       /** The top-level AST block, for which the flow-chart will be computed. */
       private Aast topLevelBlock;

       /** The stack with blocks having ON STOP phrases. */
       private Deque<FlowAstNode> stopStack = new ArrayDeque<>();

       /** The stack with blocks having ON QUIT phrases. */
       private Deque<FlowAstNode> quitStack = new ArrayDeque<>();

       /** The stack of created EXIT nodes. */
       private Stack<FlowNode> exitStack = new Stack<>();

       /** Map of AST IDs to the callgraph vertex. */
       private Map<Long, Vertex> callsites = null;

       /**
        * Setup a new flow chart for the given ast.
        *
        * @param   ast
        *          The top-level AST.
        * @param   flowDir
        *          The flow direction: vertical (when <code>true</code>) or horizontal, otherwise.
        */
       FlowChart(Aast ast, boolean flowDir)
       {
          // get all call-sites parented by this top level block
          callsites = CallGraphHelper.findCallSites(ast);

          this.flowDir = flowDir;

          topLevelBlock = ast;

          nodes = new LinkedList<>();
          links = new LinkedList<>();
       }

       /**
        * Build the flow chart snippet, starting from the {@link #topLevelBlock} block.
        *
        * @return   The {@link FlowChartSnippet}.
        */
       FlowChartSnippet build()
       {
          // in some cases, the AST needs to be prepared
          int type = topLevelBlock.getType();

          if (type == KW_PROC     ||
              type == KW_FUNCT    ||
              type == METHOD_DEF  ||
              type == KW_CONSTRUC ||
              type == KW_DESTRUCT ||
              type == KW_CLASS    ||
              type == KW_INTERFAC ||
              type == KW_ENUM)
          {
             topLevelBlock = topLevelBlock.getParent();
          }

          loadFlowNodes(null, topLevelBlock, 0);

          // some sanitize - remove links for which target/source are the same
          Iterator<FlowLink> iter = links.iterator();
          while (iter.hasNext())
          {
             FlowLink l = iter.next();
             if (l.getSource() == l.getTarget())
             {
                iter.remove();
             }
          }

          return new FlowChartSnippet(nodes, links);
       }

       /**
        * Load the flow chart starting from the given root node.
        *
        * @param    froot
        *           The current root node (may be a block or a branching statement).  For the top-level
        *           block, this will be <code>null</code>.
        * @param    root
        *           The root AST node.
        *
        * @return   The new depth.
        */
       private int loadFlowNodes(FlowAstNode froot, Aast root, int depth)
       {
          return loadFlowNodes(froot, root, "", depth);
       }

       /**
        * Load the flow chart starting from the given root node.
        *
        * @param    froot
        *           The current root node (may be a block or a branching statement).  For the top-level
        *           block, this will be <code>null</code>.
        * @param    root
        *           The root AST node.
        * @param    label
        *           The label used to link the exit nodes to the next statement.
        * @param    depth
        *           The current depth in the flow.
        *
        * @return   The new depth.
        */
       private int loadFlowNodes(FlowAstNode froot, Aast root, String label, int depth)
       {
          if (root == null)
          {
             throw new NullPointerException("Invalid root node!");
          }

          Aast flowRoot = root;
          if (flowRoot.getType() == INNER_BLOCK)
          {
             flowRoot = root.getImmediateChild(blockTypes, null);
          }

          if (flowRoot == null)
          {
             return depth;
          }

          int rtype = root.getType();
          boolean isBlock = rtype == INNER_BLOCK || froot == null;

          if (rtype == KW_METHOD || rtype == KW_CONSTRUC || rtype == KW_DESTRUCT)
          {
             if (root.getParent().downPath(BLOCK))
             {
                root = root.getParent().getImmediateChild(BLOCK, null);
             }
          }

          // the start node is created before the normal node
          if (root.getType() != BLOCK && root.downPath(BLOCK))
          {
             root = root.getImmediateChild(BLOCK, null);
          }

          Integer groupId = (froot == null ? null
                                           : (isBlock ? Integer.valueOf(froot.getId())
                                                      : froot.getGroupId()));
          Integer parentId = (froot == null ? null : froot.getId());

          // create the start/end nodes
          FlowNode start = null;
          if (isBlock)
          {
             if (froot == null)
             {
                start = createNode(root, depth++, parentId, groupId, false);
                start.setType("START");
             }
             else
             {
                start = startNodes.get(froot.getId());

                if (start == null)
                {
                   start = createNode("START", depth++, groupId);
                   start.setParentId(parentId);
                   startNodes.put(froot.getId(), start);
                }
                else
                {
                   start.setGroupId(groupId);
                   start.setParentId(parentId);
                }
             }
          }

          int numChildren = root.getNumImmediateChildren();

          FlowNode lastFlow = (isBlock ? start : froot);
          boolean isLastFlowExit = true;

          if (froot != null)
          {
             if (froot.ast.downPath(KW_ON, KW_QUIT))
             {
                quitStack.push(froot);
             }
             if (froot.ast.downPath(KW_ON, KW_STOP))
             {
                stopStack.push(froot);
             }
          }

          exitStack.push(null);

          int firstNode = nodes.size();

          // the 'root' AST is always a BLOCK node - this ensures that what we are walking are always
          // STATEMENT or INNER_BLOCK - this will not allow to go inside another node, like a
          // PROCEDDURE or FUNCTION defined within another block
          for (int i = 0; i < numChildren; i++)
          {
             Aast child = root.getChildAt(i);

             Aast flowAst = findFlowAst(child);

             if (flowAst == null)
             {
                continue;
             }

             FlowAstNode node = astsToNodes.get(flowAst.getId());
             if (node == null)
             {
                if (froot == null || flowAst.getId() != froot.ast.getId())
                {
                   node = createNode(flowAst, depth++, parentId, groupId);
                }
                else
                {
                   node = froot;
                }
             }

             Set<FlowNode> lastExits = lastFlow.exits();
             if (!lastExits.isEmpty())
             {
                linkExitNodes(lastExits, node, label);
             }
             else
             {
                FlowLink link = createLink(lastFlow, node, label, null);
                link.setExit(true);
             }

             lastFlow = node;
             isLastFlowExit = flowAst.getType() != KW_NEXT;

             // from common-progress.rules:flow_control_usage
             switch (flowAst.getType())
             {
                // flow_control_usage
                case KW_IF:
                {
                   int ibegin = nodes.size();

                   Aast aThen = null;
                   Aast aElse = null;

                   // we need the IF and first child of THEN and ELSE
                   String expr = dumpExpression((Aast) flowAst.getFirstChild());
                   node.setText("IF (" + expr + ")");

                   int ifDepth = depth;
                   aThen = findBlock(flowAst.getImmediateChild(KW_THEN, null));
                   aElse = findBlock(flowAst.getImmediateChild(KW_ELSE, null));

                   if (aElse != null && flowDir)
                   {
                      depth = processBranch(aElse, ifDepth, depth, node, groupId, "ELSE", "ELSE");
                   }

                   if (aThen != null)
                   {
                      depth = processBranch(aThen, ifDepth, depth, node, groupId, "THEN", "THEN");
                   }

                   if (aElse != null && !flowDir)
                   {
                      depth = processBranch(aElse, ifDepth, depth, node, groupId, "ELSE", "ELSE");
                   }

                   if (aElse == null)
                   {
                      // no ELSE, this needs to link to the next statement. we treat the IF as its
                      // own exit
                      node.addExit(node);
                   }
                   else
                   {
                      isLastFlowExit = false;
                   }

                   if (ibegin != nodes.size())
                   {
                      node.setBegin(ibegin);
                      node.setEnd(nodes.size() - 1);
                   }

                   break;
                }

                case KW_CASE:
                {
                   int cbegin = nodes.size();
                   int caseDepth = depth;

                   Aast aOther = findBlock(flowAst.getImmediateChild(KW_OTHER, null));

                   if (aOther != null && flowDir)
                   {
                      depth = processBranch(aOther, caseDepth, depth, node, groupId, "OTHERWISE", "ELSE");
                   }

                   // we need the CASE and all the KW_THEN plus KW_OTHER
                   // KW_WHEN will be the label (condition) for the KW_CASE -WHEN-> KW_THEN links

                   String expr = dumpExpression((Aast) flowAst.getFirstChild());
                   node.setText("CASE (" + expr + ")");

                   Aast aThen = flowAst.getImmediateChild(KW_THEN, null);
                   while (aThen != null)
                   {
                      expr = dumpExpression((Aast) aThen.getPrevSibling().getFirstChild());

                      Aast bThen = findBlock(aThen);
                      depth = processBranch(bThen, caseDepth, depth, node, groupId, "WHEN " + expr, "THEN");

                      aThen = flowAst.getImmediateChild(KW_THEN, aThen);
                   }

                   if (aOther != null && !flowDir)
                   {
                      depth = processBranch(aOther, caseDepth, depth, node, groupId, "OTHERWISE", "ELSE");
                   }

                   if (aOther == null)
                   {
                      // no otherwise, we treat the CASE as its own exit
                      node.addExit(node);
                   }
                   else
                   {
                      isLastFlowExit = false;
                   }

                   node.setBegin(cbegin);
                   node.setEnd(nodes.size() - 1);
                   break;
                }

                case KW_LEAVE:
                case KW_NEXT:
                case KW_RETURN:
                   node.setText(node.getText().toUpperCase());
                   // no other sibling of this node will get executed
                   i = numChildren;
                   depth = processTarget(froot, node, flowAst, depth);
                   break;

                case KW_STOP:
                case KW_QUIT:
                   // no other sibling of this node will get executed
                   node.setText(node.getText().toUpperCase());
                   i = numChildren;
                   depth = processTarget(froot, node, flowAst, depth);
                   break;

                // structured_error_block_ref - these are block-type statements
                case KW_CATCH:
                   node.setText(node.getText().toUpperCase());
                   depth = Math.max(depth, loadFlowNodes(node, child, "CATCH", depth));
                   break;
                case KW_FINALLY:
                   node.setText(node.getText().toUpperCase());
                   depth = Math.max(depth, loadFlowNodes(node, child, "FINALLY", depth));
                   break;

                // inner_block_ref - these are block-type statements
                case KW_DO:
                case KW_REPEAT:
                case KW_FOR:
                   depth = loadBlockFlow(depth, flowAst, child, (FlowBlockNode) node);
                   break;

                default:
                   // in this case, we have a call-site statement
                   Vertex v = callsites.get(node.ast.getId());
                   if (v != null)
                   {
                      // find the target edge
                      Vertex[] target = new Vertex[1];

                      v.edges(Direction.OUT, "raises", "calls")
                       .forEachRemaining(e -> target[0] = e.inVertex());

                      if (target[0] != null)
                      {
                         node.setCallSite(CallGraphApi.buildCallSite(target[0]));
                      }
                      int callType = (Integer) CallGraphHelper.property(v, "node-type");
                      node.setType(ProgressParser.lookupTokenName(callType));
                   }
                   node.setText(dumpExpression(node.ast));

                   lastFlow.addExit(node);
                   break;
             }
          }

          if (isBlock)
          {
             Aast whileAst = null;
             Aast toAst = null;
             boolean isLoop = false;
             FlowNode end = null;

             if (froot != null)
             {
                Aast flowRootAst = froot.ast;
                whileAst = flowRootAst.getImmediateChild(KW_WHILE, null);
                toAst = flowRootAst.getImmediateChild(KW_TO, null);
                isLoop = (whileAst != null || toAst != null);
             }

             if (froot != null && !isLoop && start == nodes.get(nodes.size() - 1))
             {
                // this is a block with no control flow
                nodes.remove(nodes.size() - 1);
                depth = depth - 1;
                start = null;

                startNodes.remove(froot.getId());
             }
             else if (lastFlow != null && (isLoop || froot == null || !lastFlow.exits().isEmpty()))
             {
                if (froot == null)
                {
                   if (exitStack.peek() == null)
                   {
                      end = createNode("EXIT", depth++, groupId);
                      exitStack.pop();
                      exitStack.push(end);
                   }
                   else
                   {
                      end = exitStack.peek();
                   }
                }
                else
                {
                   end = createNode("END", depth++, groupId);
                }
                end.setParentId(lastFlow.getParentId());

                linkExitNodes(lastFlow.exits(), end, "");
                end.addExits(lastFlow.exits());

                // link the exits to the end node
                if (isLastFlowExit)
                {
                   FlowLink link = createLink(lastFlow, end);
                   link.setExit(true);
                }
             }
          }

          if (froot != null                                              &&
              (!isBlock || !lastFlow.exits().isEmpty() || start == null) &&
              (exitStack.peek() == null || !exitStack.peek().exits().contains(froot)))
          {
             froot.addExit(froot);
          }

          exitStack.pop();

          if (froot != null)
          {
             if (froot.ast.downPath(KW_ON, KW_QUIT))
             {
                quitStack.pop();
             }
             if (froot.ast.downPath(KW_ON, KW_STOP))
             {
                stopStack.pop();
             }
          }

          int lastNode = nodes.size();
          if (froot != null)
          {
             froot.setBegin(firstNode);
             froot.setEnd(lastNode - 1);
          }

          return depth;
       }

       /**
        * Load the specified block in the flow chart.
        *
        * @param    depth
        *           The current depth.
        * @param    flowAst
        *           The AST to be included in the flow.
        * @param    child
        *           The flowAst's ancestor, which was iterated originally in current block's children.
        * @param    node
        *           A new block node, for which the entire code set needs to be loaded.
        *
        * @return   The new depth.
        */
       private int loadBlockFlow(      int           depth,
                                 final Aast          flowAst,
                                 final Aast          child,
                                 final FlowBlockNode node)
       {
          int begin = nodes.size();

          Aast lblDef = flowAst.getParent().getImmediateChild(LABEL_DEF, null);
          if (lblDef != null)
          {
             String lbl = lblDef.getText().toUpperCase();
             labelTargets.put(lbl, child);

             node.setBlockLabel(lbl);
          }

          Aast whileAst = flowAst.getImmediateChild(KW_WHILE, null);
          Aast toAst = flowAst.getImmediateChild(KW_TO, null);
          boolean isLoop = (whileAst != null || toAst != null);

          // data for "WHILE expr"
          FlowAstNode whileFlow = null;
          // data for "v = ex1 TO ex2 [ BY k ]"
          FlowAstNode assignFlow = null;
          FlowAstNode exprFlow = null;
          FlowNode incFlow = null;
          FlowNode start = null;

          // the nodes are created now, but the links are created after the body is processed
          if (isLoop)
          {
             if (whileAst != null)
             {
                start = startNodes.get(node.getId());
                if (start == null)
                {
                   start = createNode("START", depth++, null);
                   startNodes.put(node.getId(), start);
                }

                whileFlow = createNode(whileAst, depth++, null,null);
                String expr = dumpExpression((Aast) whileAst.getFirstChild());
                whileFlow.setText("WHILE (" + expr + ")");
                whileFlow.setType("KW_IF");

                whileFlow.setLoop(true);
             }

             if (toAst != null)
             {
                start = startNodes.get(node.getId());
                if (start == null)
                {
                   start = createNode("START", depth++, null);
                   startNodes.put(node.getId(), start);
                }

                Aast toExpr1Ast = (Aast) toAst.getFirstChild();
                Aast toExpr2Ast = (Aast) toExpr1Ast.getNextSibling();

                assignFlow = createNode(toExpr1Ast, depth++, null, null);
                String expr = dumpExpression(toExpr1Ast);
                assignFlow.setText(expr);

                exprFlow = createNode(toExpr2Ast, depth++, null, null);
                expr = dumpExpression(toExpr2Ast);
                exprFlow.setType("KW_IF");

                Aast toCounterAst = (Aast) toAst.getFirstChild().getFirstChild();
                String txt = toCounterAst.getText();

                exprFlow.setText("IF (" + txt + " <= " + expr + ")");

                assignFlow.setLoop(true);
                exprFlow.setLoop(true);
             }
          }

          int prevSize = nodes.size();
          depth = Math.max(depth, loadFlowNodes(node, child, depth));
          int nextSize = nodes.size();

          FlowNode bstart = start;
          FlowNode bend;
          FlowLink bstartLink = null;

          if (isLoop)
          {
             bend = nodes.get(nextSize - 1);

             // find the link from bstart
             for (int j = links.size() - 1; j >= 0; j--)
             {
                FlowLink l = links.get(j);

                if (bstartLink == null && l.getSource() == start.getId())
                {
                   bstartLink = l;
                   break;
                }
             }
          }
          else
          {
             bend = null;
          }

          if (whileAst != null)
          {
             // re-adjust parent/group IDs
             whileFlow.setParentId(start.getParentId());
             whileFlow.setGroupId(start.getGroupId());

             // re-link the start/end and other nodes

             // WHILE expr
             /*
             [start]
                |
                |  /------------------------------------\
                v  V v-----------------[next]            |
           /-----------\                |                |
          |    expr    | -- yes -> [the block's code] --/
           \-----------/                /
                |                      |
                no                    leave]
                v                      |
              [end]  <----------------/
              */
             whileFlow.setBegin(prevSize);
             whileFlow.setEnd(nextSize - 2);

             // link the start to the expr
             createLink(start, whileFlow);

             // re-link the start, instead of [start] -> [block], use [expr] -then-> [block]
             bstartLink.setSource(whileFlow.getId());
             bstartLink.setExit(false);
             bstartLink.setType("THEN");
             bstartLink.setText("THEN");
             int parentId = nodes.get(bstartLink.getTarget()).getParentId();
             for (int i = prevSize; i < nextSize; i++)
             {
                FlowNode n = nodes.get(i);

                // all block code is parented at the THEN
                if (n.getParentId() == parentId)
                {
                   n.setParentId(whileFlow.getId());
                }

                // remove the END from the block's code, and leave it at the block
                if (n instanceof FlowAstNode)
                {
                   FlowAstNode anode = (FlowAstNode) n;

                   if (anode.getEnd() != null && anode.getEnd() == nextSize - 1)
                   {
                      anode.setEnd(anode.getEnd() - 1);
                   }
                }
             }

             // TODO: if there is no code, add the first statement line
             // TODO: this code needs to have the depth between START and END

             // re-link the NEXT and RETRY links which target the START node
             int whileFlowId = whileFlow.getId();
             links.stream()
                  .filter(l -> l.getTarget() == bstart.getId() &&
                               ("KW_NEXT".equals(l.getType()) || "KW_RETRY".equals(l.getType())))
                  .forEach(l -> l.setTarget(whileFlowId));

             // normal, non-LEAVE links targeting END must link to the whileFlowId
             links.stream()
                  .filter(l -> l.getTarget() == bend.getId() &&
                               !"KW_LEAVE".equals(nodes.get(l.getSource()).getType()))
                  .forEach(l -> { l.setTarget(whileFlowId); l.setType("KW_NEXT"); });

             // create a ELSE link to the END node
             FlowLink link = createLink(whileFlow, bend, "", "");
             link.setExit(true);
          }

          if (toAst != null)
          {
             // create the 'incexpr'
             Aast toByAst    = toAst.getImmediateChild(KW_BY, null);
             Aast toCounterAst = (Aast) toAst.getFirstChild().getFirstChild();
             String txt = toCounterAst.getText();
             String incTxt = "1";
             if (toByAst != null)
             {
                incTxt = dumpExpression((Aast) toByAst.getFirstChild());
             }
             incTxt = txt + " = " +  txt + " + " + incTxt;

             incFlow = createNode(incTxt, depth++, null);
             incFlow.setLoop(true);

             // switch the depth between the end and incFlow nodes
             int d1 = bend.getDepth();
             int d2 = incFlow.getDepth();
             bend.setDepth(d2);
             incFlow.setDepth(d1);

             // re-adjust parent/group IDs
             assignFlow.setParentId(start.getParentId());
             assignFlow.setGroupId(start.getGroupId());

             exprFlow.setParentId(start.getParentId());
             exprFlow.setGroupId(start.getGroupId());

             incFlow.setParentId(start.getParentId());
             incFlow.setGroupId(start.getGroupId());

             // re-link the start/end and other nodes

             // v = expr1 to expr2 [by k]
             /*
                 [start]
                    |
                    |
                    V
                [v = expr1]
                    |
                    |    /------------------------------------------------------\
                    v    V                                                       |
                  /-----------\                                                  |
                 | v <= expr2  | -- yes -> [the block's code] --> [ v = v + k ] /
                  \-----------/              /    \-------[next]----------^
                       |                     |
                       no                    |
                       v                    /
                     [end] <-------[leave]--
              */
             exprFlow.setBegin(prevSize);
             exprFlow.setEnd(nextSize - 1);

             // link the start to the assignFlow
             createLink(start, assignFlow);

             // link the assignFlow to the exprFlow
             createLink(assignFlow, exprFlow);

             // link the incFlow to the exprFlow - this is a NEXT
             createLink(incFlow, exprFlow, "", "KW_NEXT");

             // re-link the start, instead of [start] -> [block], use [expr] -then-> [block]
             bstartLink.setSource(exprFlow.getId());
             bstartLink.setExit(false);
             bstartLink.setType("THEN");
             bstartLink.setText("THEN");
             int parentId = nodes.get(bstartLink.getTarget()).getParentId();
             for (int i = prevSize; i < nextSize; i++)
             {
                FlowNode n = nodes.get(i);

                // all block code is parented at the THEN
                if (n.getParentId() == parentId)
                {
                   n.setParentId(exprFlow.getId());
                }

                // remove the END from the block's code, and leave it at the block
                if (n instanceof FlowAstNode)
                {
                   FlowAstNode anode = (FlowAstNode) n;

                   if (anode.getEnd() != null && anode.getEnd() == nextSize - 1)
                   {
                      anode.setEnd(anode.getEnd() - 1);
                   }
                }
             }

             int exprFlowId = exprFlow.getId();
             int incFlowId = incFlow.getId();

             // re-link the NEXT links which target the START node - these will target incFlow
             links.stream()
                  .filter(l -> l.getTarget() == bstart.getId() && "KW_NEXT".equals(l.getType()))
                  .forEach(l -> l.setTarget(incFlowId));

             // RETRY links which target the START node will be target exprFlow
             links.stream()
                  .filter(l -> l.getTarget() == bstart.getId() && "KW_RETRY".equals(l.getType()))
                  .forEach(l -> l.setTarget(exprFlowId));

             // normal, non-LEAVE links targeting END must link to the incFlow
             links.stream()
                  .filter(l -> l.getTarget() == bend.getId() &&
                               !"KW_LEAVE".equals(nodes.get(l.getSource()).getType()))
                  .forEach(l -> l.setTarget(incFlowId));

             // create a ELSE link to the END node
             FlowLink link = createLink(exprFlow, bend, "", "");
             link.setExit(true);
          }

          node.setBegin(begin);
          node.setEnd(nodes.size() - 1);

          if (node.getBlockLabel() != null)
          {
             labelTargets.remove(node.getBlockLabel());
          }

          return depth;
       }

       /**
        * Dump this ASTs as a 4GL code snippet.
        *
        * TODO: replace with anti-parser code.
        *
        * @param    node
        *           The AST to dump.
        *
        * @return   The anti-parsed AST text.
        */
       private String dumpExpression(Aast node)
       {
          if (node.getParent().getType() == STATEMENT)
          {
             String txt = node.getText();
             Aast child = (Aast) node.getFirstChild();
             while (child != null)
             {
                txt = txt + " " + dumpExpression(child);
                child = (Aast) child.getNextSibling();
             }

             return txt;
          }

          if (node.getType() != EXPRESSION)
          {
             return node.getText();
          }

          String txt = "";
          if (node.getLine() != 0 && node.getColumn() != 0 && node.getFirstChild() == null)
          {
             txt = node.getText();
          }

          Aast child = (Aast) node.getFirstChild();
          while (child != null)
          {
             int type = child.getType();

             if (type >= BEGIN_FUNCTYPES && type <= END_FUNCTYPES)
             {
                txt = txt + child.getText() + "(";
                Aast fchild = (Aast) child.getFirstChild();
                boolean first = true;
                while (fchild != null)
                {
                   String expr = dumpExpression(fchild);
                   if (expr == null)
                   {
                      continue;
                   }

                   txt = txt + (first ? "" : ",") + expr;
                   first = false;

                   fchild = (Aast) fchild.getNextSibling();
                }
                txt = txt + ")";
             }
             else if (type == EQUALS || type == NOT_EQ ||
                      type == GT || type == LT || type == GTE || type == LTE ||
                      type == PLUS || type == MINUS || type == MULTIPLY || type == DIVIDE ||
                      type == KW_MOD ||
                      type == KW_OR || type == KW_AND ||
                      type == COLON || type == ASSIGN)
             {
                txt = txt + dumpExpression((Aast) child.getFirstChild()) + " ";
                txt = txt + child.getText() + " ";
                txt = txt + dumpExpression((Aast) child.getFirstChild().getNextSibling()) + " ";
             }
             else if (type == KW_NOT)
             {
                txt = txt + child.getText() + " ";
                txt = txt + dumpExpression((Aast) child.getFirstChild()) + " ";
             }
             else
             {
                txt = txt + dumpExpression(child);
             }

             child = (Aast) child.getNextSibling();
          }

          return txt.trim();
       }

       /**
        * If an AST which can be included in the flow chart, from the specified node's descendants.
        *
        * @param    node
        *           The node which needs to be processed.
        *
        * @return   If the node has a direct descendant one of the {@link #statementTypes},
        *           {@link #blockTypes} or {@link #callsites} ASTs, return it.
        *           Otherwise, return <code>null</code>.
        */
       private Aast findFlowAst(Aast node)
       {
          int type = node.getType();
          if (type == STATEMENT)
          {
             Aast ret = node.getImmediateChild(statementTypes, null);

             if (ret != null && ret.getType() == KW_UNDO)
             {
                // discard the UNDO, use only its associated LEAVE/NEXT/RETRY/RETURN
                ret = ret.getImmediateChild(statementTypes, null);
             }

             if (ret == null)
             {
                ret = (Aast) node.getFirstChild();

                ret = callsites.containsKey(ret.getId()) ? ret : null;
             }

             return ret;
          }
          else if (type == INNER_BLOCK)
          {
             return node.getImmediateChild(blockTypes, null);
          }
          else
          {
             return null;
          }
       }

       /**
        * Find the block associated with this AST.
        *
        * @param    node
        *           The source AST.
        *
        * @return   The {@link ProgressParserTokenTypes#BLOCK} AST.
        */
       private Aast findBlock(Aast node)
       {
          if (node == null)
          {
             return null;
          }

          int type = node.getType();
          if (type == BLOCK || type == INNER_BLOCK)
          {
             return node;
          }

          if (node.downPath(INNER_BLOCK))
          {
             node = node.getImmediateChild(INNER_BLOCK, null);
          }
          else if (type != KW_FINALLY && type != KW_CATCH)
          {
             node = node.getImmediateChild(BLOCK, null);
          }

          return node;
       }

       /**
        * Process a THEN/ELSE or WHEN/OTHERWISE branch, recursively, until the branch has finished.
        *
        * @param   ast
        *          The first AST in the branch.
        * @param   pdepth
        *          The parent's depth.
        * @param   depth
        *          The current depth.
        * @param   node
        *          The node from which branching originates.
        * @param   groupId
        *          The group to which the branch belongs.
        * @param   elabel
        *          The edge label to link the node to the branch.
        * @param   etype
        *          The edge type to link the node to the branch.
        *
        * @return  The new depth.
        */
       private int processBranch(Aast        ast,
                                 int         pdepth,
                                 int         depth,
                                 FlowAstNode node,
                                 Integer     groupId,
                                 String      elabel,
                                 String      etype)
       {
          FlowAstNode fnode = createNode(ast, pdepth, node.getId(), groupId);
          createLink(node, fnode, elabel, etype);
          int begin = nodes.size();

          if (fnode instanceof FlowBlockNode)
          {
             depth = Math.max(depth, loadBlockFlow(pdepth + 1, fnode.ast, ast, (FlowBlockNode) fnode));
          }
          else
          {
             depth = Math.max(depth, loadFlowNodes(fnode, ast, pdepth + 1));
          }

          node.addExits(fnode.exits());
          if (begin != nodes.size())
          {
             fnode.setBegin(begin);
             fnode.setEnd(nodes.size() - 1);
          }
          else
          {
             fnode.setBegin(null);
             fnode.setEnd(null);
          }

          return depth;
       }

       /**
        * Given a statement which may switch execution to a previous statement in a flow, find the
        * target and link to it.
        *
        * @param    block
        *           The block to which this node belongs.
        * @param    node
        *           The statement node.
        * @param    ast
        *           The associated AST.
        * @param    depth
        *           The next depth value.
        *
        * @return   The new depth value.
        */
       private int processTarget(FlowNode block, FlowNode node, Aast ast, int depth)
       {
          Aast targetAst = null;
          int type = ast.getType();
          String edgeLbl = "";

          if (type == KW_QUIT)
          {
             if (!quitStack.isEmpty())
             {
                targetAst = quitStack.peek().ast;
             }
             edgeLbl = "QUIT";
          }
          else if (type == KW_STOP)
          {
             if (!stopStack.isEmpty())
             {
                targetAst = stopStack.peek().ast;
             }
             edgeLbl = "STOP";
          }
          else if (type == KW_RETURN)
          {
             // no label here
          }
          else
          {
             // in LEAVE/NEXT case, resolve the label
             Aast lbl = ast.getImmediateChild(LABEL, null);
             if (lbl == null && type == KW_RETRY)
             {
                // if not specified, inherit from UNDO
                lbl = ast.getParent().getImmediateChild(LABEL, null);
             }

             if (lbl != null)
             {
                targetAst = labelTargets.get(lbl.getText().toUpperCase());
                edgeLbl = lbl.getText().toUpperCase();
             }

             if (targetAst == null)
             {
                // see block_properties.rules lines 1280 and 1325 for conversion-time resolution for
                // LEAVE and NEXT without a label

                if (type == KW_NEXT || type == KW_RETRY)
                {
                   // find first loop block
                   targetAst = findBlockAncestor(ast, a -> a.downPath(KW_REPEAT)  ||
                                                      a.downPath(KW_FOR, KW_EACH) ||
                                                      a.downPath(KW_DO, KW_TO)    ||
                                                      a.downPath(KW_DO, KW_WHILE));

                   // if not found, find first FOR FIRST/LAST block
                   if (targetAst == null)
                   {
                      targetAst = findBlockAncestor(ast, a -> a.downPath(KW_FOR, KW_FIRST) ||
                                                         a.downPath(KW_FOR, KW_LAST));
                   }

                   // if still not found, act like RETURN
                }
                else if (type == KW_LEAVE)
                {
                   targetAst = findBlockAncestor(ast, a -> a.downPath(KW_REPEAT)   ||
                                                      a.downPath(KW_FOR)           ||
                                                      a.downPath(KW_DO, KW_TO)     ||
                                                      a.downPath(KW_DO, KW_WHILE));

                   // if still not found, act like RETURN
                }
             }

             if (targetAst != null)
             {
                int ttype = targetAst.getType();
                if (type == KW_NEXT &&
                    ttype == KW_FOR &&
                    (targetAst.downPath(KW_FIRST) || targetAst.downPath(KW_LAST)))
                {
                   // this is a leave
                   targetAst = null;
                }
             }

             if (targetAst == null)
             {
                edgeLbl = "RETURN";
             }
             else if (type == KW_NEXT)
             {
                edgeLbl = "NEXT " +  edgeLbl;
                node.setText(edgeLbl);
             }
             else if (type == KW_LEAVE)
             {
                edgeLbl = "LEAVE " +  edgeLbl;
                node.setText(edgeLbl);
             }
             else if (type == KW_RETRY)
             {
                edgeLbl = "RETRY " +  edgeLbl;
                node.setText(edgeLbl);
             }

             node.setText(node.getText().trim());
             edgeLbl = edgeLbl.trim();
          }

          if (targetAst == null)
          {
             // in this case, we are exiting the start node (RETURN or LEAVE/NEXT converted to RETURN)
             FlowNode target = createNode("EXIT", depth++, node.getGroupId());
             target.setParentId(node.getParentId());

             FlowLink link = createLink(node, target, edgeLbl, null);
             link.setExit(true);

             exitStack.pop();
             exitStack.push(target);
             target.addExit(node);
          }
          else
          {
             targetAst = findFlowAst(targetAst);
             FlowNode target = astsToNodes.get(targetAst.getId());

             // depending on the statement, we are targeting the target or the target's sibling...
             if (type == KW_NEXT || type == KW_RETRY)
             {
                target = startNodes.get(target.getId());

                // this is a 'next' iteration, linking to the START node
                String stype = type == KW_NEXT ? "KW_NEXT" : "KW_RETRY";
                createLink(node, target, edgeLbl, stype);
                target.setLoop(true);
             }
             else if (type == KW_LEAVE)
             {
                target.addExit(node);
             }
             else if(type == KW_STOP || type == KW_QUIT)
             {
                // we are targeting the next statement, this is an exit for the current block
                block.addExit(node);
             }
          }

          return depth;
       }

       /**
        * Find an AST node with line/column annotations referring to a valid legacy code.
        *
        * @param    ast
        *           The AST node.
        *
        * @return   An AST node indicating the line/column of this code.
        */
       private Aast findNodeSource(Aast ast)
       {
          // find first node which has the parent annotated with line/column
          Aast prevParent = ast;
          Aast lastp = ast;
          while (lastp != null)
          {
             if (lastp.getLine() != 0)
             {
                break;
             }

             if (lastp.getType() == INNER_BLOCK)
             {
                Aast ch = lastp.getImmediateChild(Arrays.asList(KW_DO, KW_FOR, KW_REPEAT), null);
                if (ch != null)
                {
                   prevParent  = ch;
                }
                else
                {
                   prevParent = lastp;
                }

                break;
             }

             prevParent = lastp;
             lastp = lastp.getParent();
          }

          // find first child which has line/column
          if (prevParent.getLine() == 0)
          {
             lastp = (Aast) prevParent.getFirstChild();
             while (lastp != null && lastp.getLine() == 0)
             {
                lastp = (Aast) lastp.getFirstChild();
             }

             if (lastp != null)
             {
                // failed to find, fallback
                ast = lastp;
             }
          }
          else
          {
             ast = prevParent;
          }

          return ast;
       }

       /**
        * Create a new {@link FlowNode node} and add it to the {@link #nodes} list.
        *
        * @param    type
        *           The node's type.
        * @param    depth
        *           The node's depth.
        * @param    groupId
        *           The node's group ID - may be <code>null</code>.
        *
        * @return   The created node.
        */
       private FlowNode createNode(String type, int depth, Integer groupId)
       {
          int nextId = nodes.size();
          FlowNode node = new FlowNode(nextId, type);
          node.setDepth(depth);
          node.setGroupId(groupId);
          nodes.add(node);

          return node;
       }

       /**
        * Create a new {@link FlowAstNode ast} or {@link FlowBlockNode block} node and add it to the
        * {@link #nodes} list.  Only FINALLY, CATCH, DO, REPEAT or FOR blocks will create a
        * {@link FlowBlockNode} instance.
        *
        * @param    ast
        *           The associated AST.
        * @param    depth
        *           The node's depth.
        * @param    parentId
        *           The node's parent ID - may be <code>null</code>.
        * @param    groupId
        *           The node's group ID - may be <code>null</code>.
        *
        * @return   The created node.
        */
       private FlowAstNode createNode(Aast ast, int depth, Integer parentId, Integer groupId)
       {
          return createNode(ast, depth, parentId, groupId, true);
       }

       /**
        * Create a new {@link FlowAstNode ast} or {@link FlowBlockNode block} node and add it to the
        * {@link #nodes} list.  Only FINALLY, CATCH, DO, REPEAT or FOR blocks will create a
        * {@link FlowBlockNode} instance.
        *
        * @param    ast
        *           The associated AST.
        * @param    depth
        *           The node's depth.
        * @param    parentId
        *           The node's parent ID - may be <code>null</code>.
        * @param    groupId
        *           The node's group ID - may be <code>null</code>.
        * @param    resolve
        *           When set, this flag will not allow to create a new node for an AST existing in the
        *           {@link #astsToNodes} map.
        *
        * @return   The created node.
        */
       private FlowAstNode createNode(Aast    ast,
                                      int     depth,
                                      Integer parentId,
                                      Integer groupId,
                                      boolean resolve)
       {
          if (ast == null)
          {
             return null;
          }

          Aast srcAst = ast;

          if (resolve)
          {
             if (ast.getType() == INNER_BLOCK)
             {
                ast = ast.getImmediateChild(blockTypes, null);
             }

             if (astsToNodes.containsKey(ast.getId()))
             {
                return astsToNodes.get(ast.getId());
             }

             srcAst = findNodeSource(ast);

             if (statementTypes.contains(srcAst.getType()))
             {
                ast = srcAst;
             }
          }
          else
          {
             srcAst = (ast.isRoot() ? ast
                                    : ast.getType() == BLOCK ? ast.getIndexPos() == 0 ? ast.getParent()
                                                                                      : ast.getPrevSibling()
                                                             : (Aast) ast.getFirstChild());
             if (srcAst.getType() == TRIGGER_BLOCK)
             {
                srcAst = srcAst.getParent();
             }
          }

          int nextId = nodes.size();
          FlowAstNode node = null;
          int type = ast.getType();

          if (type == KW_FINALLY ||
              type == KW_CATCH   ||
              type == KW_DO      ||
              type == KW_REPEAT  ||
              type == KW_FOR)
          {
             node = new FlowBlockNode(nextId,
                                      srcAst,
                                      ProgressParser.lookupTokenName(srcAst.getType()),
                                      srcAst.getText(),
                                      srcAst.getLine(),
                                      srcAst.getColumn());

             computeBlockHeader((FlowBlockNode) node);
          }
          else
          {
             node = new FlowAstNode(nextId,
                                    srcAst,
                                    ProgressParser.lookupTokenName(srcAst.getType()),
                                    srcAst.getText(),
                                    srcAst.getLine(),
                                    srcAst.getColumn());
          }
          nodes.add(node);

          node.setDepth(depth);
          AtomicInteger order = depthOrder.get(depth);
          if (order == null)
          {
             depthOrder.put(depth,  order = new AtomicInteger(0));
          }
          node.setOrder(order.getAndIncrement());

          astsToNodes.put(ast.getId(), node);

          node.setParentId(parentId);
          node.setGroupId(groupId);

          return node;
       }

       /**
        * Compute the header for this block node - this includes resolving any ON phrases, WHERE or
        * TO expression, FRAME name or buffer details.
        *
        * @param    node
        *           The block node to augment with header details.
        */
       private void computeBlockHeader(FlowBlockNode node)
       {
          Aast ast = node.ast;

          // ON phrases: QUIT, STOP, ENDKEY, ENDERROR
          Aast on = ast.getImmediateChild(KW_ON, null);
          while (on != null)
          {
             String phrase = "";
             Aast undoAst = on.getChildAt(on.getNumImmediateChildren() - 2);
             Aast actionAst = on.getChildAt(on.getNumImmediateChildren() - 1);

             phrase = undoAst.getText().toUpperCase();
             if (undoAst.downPath(LABEL))
             {
                phrase = phrase + " " + undoAst.getFirstChild().getText();
             }
             phrase = phrase + ", " + actionAst.getText().toUpperCase();
             if (actionAst.downPath(LABEL))
             {
                phrase = phrase + " " + actionAst.getFirstChild().getText();
             }

             if (on.downPath(KW_QUIT))
             {
                node.setOnQuit(phrase);
             }
             else if (on.downPath(KW_STOP))
             {
                node.setOnStop(phrase);
             }
             else if (on.downPath(KW_ERROR))
             {
                node.setOnError(phrase);
             }
             else if (on.downPath(KW_ENDKEY))
             {
                node.setOnEndkey(phrase);
             }

             node.setWithHeader(true);

             on = ast.getImmediateChild(KW_ON, on);
          }

          // LOOP expr: WHILE, TO
          if (ast.downPath(KW_WHILE))
          {
             Aast whileAst = ast.getImmediateChild(KW_WHILE, null);
             node.setWhileExpr(dumpExpression((Aast) whileAst.getFirstChild()));
             node.setWithHeader(true);
          }
          if (ast.downPath(KW_TO))
          {
             Aast toAst = ast.getImmediateChild(KW_TO, null);
             node.setToExpr(dumpExpression((Aast) toAst.getFirstChild()));
             node.setWithHeader(true);
          }

          // buffers: RECORD_PHRASE  gives the buffer, prev sibling gives the type
          Aast bast = ast.downPath(KW_PRESEL) ? ast.getImmediateChild(KW_PRESEL, null) : ast;
          Aast recordPhrase = bast.getImmediateChild(RECORD_PHRASE, null);
          String buffers = null;
          while (recordPhrase != null)
          {
             Aast prev = recordPhrase.getPrevSibling();

             String phrase = prev.getText().toUpperCase() + " " + recordPhrase.getFirstChild().getText();
             if (buffers == null)
             {
                buffers = phrase;
             }
             else
             {
                buffers = buffers + ", " + phrase;
             }

             recordPhrase = ast.getImmediateChild(RECORD_PHRASE, recordPhrase);
          }
          node.setBuffers(buffers);
          if (buffers != null)
          {
             node.setWithHeader(true);
             node.setText(node.getText() + " " + buffers);
          }

          // TRANSACTION:
          node.setTransaction(ast.downPath(KW_TRANS));
          if (node.isTransaction())
          {
             node.setWithHeader(true);
          }

          // frame phrase
          if (ast.downPath(FRAME_PHRASE, KW_FRAME, WID_FRAME))
          {
             node.setFrame(ast.getImmediateChild(FRAME_PHRASE, null)
                              .getImmediateChild(KW_FRAME, null)
                              .getImmediateChild(WID_FRAME, null)
                              .getText());

             node.setWithHeader(true);
          }
       }

       /**
        * Create a new link between the specified source and target nodes, and add it to the
        * {@link #links} list.  Use an empty label and a <code>null</code> link type.
        *
        * @param    source
        *           The source node.
        * @param    target
        *           The target node.
        *
        * @return   The created link.
        */
       private FlowLink createLink(FlowNode source, FlowNode target)
       {
          return createLink(source, target, "", null);
       }

       /**
        * Create a new link between the specified source and target nodes, and add it to the
        * {@link #links} list.
        *
        * @param    source
        *           The source node.
        * @param    target
        *           The target node.
        * @param    label
        *           The link's label.
        * @param    type
        *           The link's type - may be <code>null</code>.
        *
        * @return   The created link.
        */
       private FlowLink createLink(FlowNode source, FlowNode target, String label, String type)
       {
          int nextId = links.size();

          FlowLink link = new FlowLink(nextId, source.getId(), target.getId(), label, type);
          links.add(link);

          return link;
       }

       /**
        * Link the specified exit nodes to the given target and mark the links as exits.
        *
        * @param    sources
        *           The list of exit nodes.
        * @param    target
        *           The next reachable node (may be a START, END or EXIT node).
        * @param    label
        *           The link's label.
        */
       private void linkExitNodes(Collection<FlowNode> sources, FlowNode target, String label)
       {
          // all remaining roots are linked to END
          for (FlowNode src : sources)
          {
             FlowLink link = createLink(src, target, label, null);
             link.setExit(true);
          }
       }

       /**
        * Find the nearest block ancestor satisfying the given predicate, without going outside of the
        * {@link #topLevelBlock}.  The explicit check of 'not going outside' of the top-level block
        * is required because procedures may be defined within other blocks (but not nested).
        *
        * @param    ast
        *           The ast from which to start looking.
        * @param    eval
        *           The predicate which needs to be evaluated.
        *
        * @return   The found ancestor satisfying the predicate, or <code>null</code> if not found.
        */
       private Aast findBlockAncestor(Aast ast, Predicate<Aast> eval)
       {
          // find first non-"simple DO block" ancestor; if non exists, convert to RETURN
          Aast targetAst = ast.getAncestor(-1, INNER_BLOCK);
          while (true)
          {
             if (targetAst == null)
             {
                break;
             }

             if (!topLevelBlock.isAncestorOf(0, targetAst))
             {
                // all found INNER_BLOCK nodes must be a child of the top-level-block we are
                // exploring; if going above it, convert to RETURN
                targetAst = null;
                break;
             }

             if (eval.test(targetAst))
             {
                // found the required block, use it
                break;
             }

             if (targetAst.isRoot())
             {
                // can't happen, but protect anyway
                targetAst = null;
                break;
             }

             targetAst = targetAst.getParent().getAncestor(-1, INNER_BLOCK);
          }

          return targetAst;
       }
    }