Search code examples
visitor-patternrascal

What exactly does the outermost visit strategy do in Rascal?


I wrote the below Rascal code that is supposed to build a tree out of a map from node names to nodes, starting at the node mapped from "top". It should repeatedly replace the children of all nodes that have strings as children in result by the nodes nodeMap maps them to, until nothing changes anymore (fixpoint).

getNode returns the node a map[str,node] maps it to, or the key itself if it is not present as a key in the map. This works fine, as proves the fact that other code at the bottom of this question does work. However, the code directly below seems to run infintely even on very small inputs.

node nodeMapToNode(map[str, node] nodeMap) {
    node result = nodeMap["top"];
    return outermost visit(result) {
        case node n: {
            if ([*str children] := getChildren(n)) {
                insert makeNode(getName(n), [getNode(child, nodeMap) | child <- children]);
            }
        }
    }
}

The following code does work and returns in an instant on small inputs as I expected. This is, however, exactly what I understood outermost-visiting should do from what I understood from the Rascal tutor.

Can anyone explain to me what the difference between these code snippets is (besides the way they are written) and what I thus misunderstood about the effect of outermost visit? Also, I'd like to know if a shorter and/or nicer way to write this code - using something like outermost-visiting instead of writing the fixpoint by hand - does exist.

node nodeMapToNode(map[str, node] nodeMap) {
    node result = nodeMap["top"];
    node lastResult;
    do {
        lastResult = result;
        result = visit(lastResult) {
            case node n: {
                if ([*str children] := getChildren(n)) {
                    insert makeNode(getName(n),
                        [getNode(child, nodeMap) | child <- children]);
                }
            }
        }
    } while (result != lastResult);
    return result;
}

Solution

  • What is outermost?

    The rascal tutor is very compact in it's explanation but let's start from there.

    repeat a top-down traversal as long as the traversal changes the resulting value (compute a fixed-point).

    which in rascal terms means that this:

    r = outermost visit(x) {
      case str s => s + "."
        when size(s) < 3
    };
    

    is syntactic sugar for:

    r = x;
    solve(r) {
      r = top-down visit(r) {
        case str s => s + "."
          when size(s) < 3
      };
    }
    

    I think there are two common cases were outermost/innermost makes sense:

    • your replacement should be repeated multiple times on the same node
    • your replacement generate new nodes that match other patterns

    Your specific example

    Regarding the example in your question. The other manually rewritten outermost is actually an innermost. The default visit strategy is bottom-up.

    In general, an bottom-up visit of the tree is a quicker than a top-down. Especially when you are rewriting it, since Rascal is immutable, building a new tree bottom-up is quicker.

    So, perhaps replace your code with an innermost visit instead of an outermost?