Computer Science 236

Project 5 Lab Session


Introduction


In this lab session you will write some of the code you need to represent a rule dependency graph. The graph will be used to represent the dependencies between the rules in a Datalog program. The graph will have a node for each rule and an edge for each dependency between rules. The goal at the end of the session is to create a graph of dependencies for a given list of rules and to print the adjacency list form of the graph.


Example


Consider the following example rules:

A(X,Y) :- B(X,Y), C(X,Y). # R0
B(X,Y) :- A(X,Y), D(X,Y). # R1
B(X,Y) :- B(Y,X).         # R2

Since there are three rules, the graph for these rules will have three nodes. The first rule has predicate B() in it's body and the second rule has predicate B() in it's head, so the first rule depends on the second rule. (In other words, evaluation of the second rule could produce tuples that are used by the first rule.) As a result the graph will have an edge from the node for the first rule to the node for the second rule. Try drawing the full dependency graph for these rules, using the labels R0, R1, and R2 for the nodes.


Rule ID's and Node ID's


We'll identify the rules in the program and the nodes in the graph with integer numbers. For example the identifiers for the rules above are 0, 1, and 2. (Assuming the rules are stored in a vector, these identifiers are the same as the indexes of the rules in the vector.)

When you print out a rule identifier or a node identifier, always print the letter R before the identifier. For example, the identifiers for the rules above should print as R0, R1, and R2. The letter R is for decoration only and is not part of the identifier.

This session has three parts, making a Node class, making a Graph class, and building a graph from a list of rules.


Part 1: Node Class


  1. Write a Node class (Node.h).

    class Node {
    
     private:
    
      set<int> adjacentNodeIDs;
    
     public:
    
      void addEdge(int adjacentNodeID) {
        adjacentNodeIDs.insert(adjacentNodeID);
      }
    
    };
    

    A Node holds a 'set' of adjacent node IDs. Each node ID in the set represents an edge in the graph. Node IDs are added to the set by calling the 'addEdge' function. (You may want add other variables to your Node class later, perhaps a visited flag or a post-order number.)

  2. Write a 'toString' function for the Node class (Node.h).

    The 'toString' function prints the node IDs in the 'adjacent node IDs' set as a comma separated list. The letter R decoration is printed before each node ID. The node IDs are printed in sorted order. (Using a 'set' makes the sorting automatic.)

    For example, if the node ID set contains the values 1, 4, 2, the 'toString' function should print:

    R1,R2,R4
    
  3. Test the 'toString' function (main.cpp)

    Write a 'main' function that creates a node, adds some edges, and prints the result.

    int main() {
      Node node;
      node.addEdge(4);
      node.addEdge(8);
      node.addEdge(2);
      cout << node.toString() << endl;
    }
    

    Compile and test. The output should look something like this:

    R2,R4,R8
    


Part 2: Graph Class


  1. Write a Graph class (Graph.h).

    class Graph {
    
     private:
    
      map<int,Node> nodes;
    
     public:
    
      Graph(int size) {
        for (int nodeID = 0; nodeID < size; nodeID++)
          nodes[nodeID] = Node();
      }
    
      void addEdge(int fromNodeID, int toNodeID) {
        nodes[fromNodeID].addEdge(toNodeID);
      }
    
    };
    

    A Graph holds a 'map' from node IDs (ints) to Nodes. Since each Node holds a set of adjacent nodes, the Graph and Node classes combined provide an adjacency list type of representation for a graph.

    The constructor is passed the number of nodes to create in the graph. Nodes are then created with node IDs from 0 to size-1.

    The 'addEdge' function grabs the 'fromNode' in the map and calls 'addEdge' on that Node to add the 'toNodeID' to the set of adjacent node IDs in that Node object, thus creating an edge in the graph.

  2. Write a 'toString' function for the Graph class (Graph.h).

    The 'toString' function prints each Node in the Graph on a separate line.

    For each Node, print the node ID for the Node (print the letter R before the ID), followed by a colon (:) and the result of the 'toString' function on the Node.

    The nodes from the graph are printed in sorted order. (Using a 'map' makes the sorting automatic.)

    A loop such as the following would allow you to access each Node in the map. Note that 'pair.first' gives you a key from the map, (which is the node ID of one of the nodes in the graph), and 'pair.second' gives you the Node object that corresponds to the key given by 'pair.first'.

        for (auto& pair: nodes) {
          int nodeID = pair.first;
          Node node = pair.second;
        }
    
  3. Test the 'toString' function (main.cpp)

    Write a 'main' function that creates a graph, adds some edges, and prints the result.

    int main() {
      Graph graph(3);
      graph.addEdge(1,2);
      graph.addEdge(1,0);
      graph.addEdge(0,1);
      graph.addEdge(1,1);
      cout << graph.toString();
    }
    

    Compile and test. The output should look something like this:

    R0:R1
    R1:R0,R1,R2
    R2:
    


Part 3: Build a Dependency Graph


  1. Write a function named 'makeGraph' in your Interpreter class.

      static Graph makeGraph(const vector<Rule>& rules) {
    
        Graph graph(rules.size());
        // add code to add edges to the graph for the rule dependencies
        return graph;
    
      }
    

    The 'makeGraph' function is given a vector of Rules and returns a Graph that represents the dependencies between the rules. (The code given in this step is just a stub for this function. You will add code in later steps to loop over the rules and find the dependencies.)

  2. Test the 'makeGraph' function.

    Write a 'main' function that creates a vector of rules and calls the 'makeGraph' function.

    int main() {
    
      // predicate names for fake rules
      // first is name for head predicate
      // second is names for body predicates
      pair<string,vector<string>> ruleNames[] = {
        { "A", { "B" } },
        { "B", { "B", "A" } },
      };
    
      vector<Rule> rules;
    
      for (auto& rulePair : ruleNames) {
        string headName = rulePair.first;
        Rule rule = Rule(Predicate(headName));
        vector<string> bodyNames = rulePair.second;
        for (auto& bodyName : bodyNames)
          rule.addBodyPredicate(Predicate(bodyName));
        rules.push_back(rule);
      }
    
      Graph graph = Interpreter::makeGraph(rules);
      cout << graph.toString();
    
    }
    

    This code assumes: (1) the Predicate class has a constructor that takes the name of the predicate as a parameter, (2) the Rule class has a constructor that takes the head predicate as a parameter, and (3) the Rule class has a function 'addBodyPredicate' that adds a predicate to the body of the rule. You may need to either adjust this code or those classes to make them work together.

    Compile and test. The output should look something like this: (this represents two nodes in the graph, but no edges yet)

    R0:
    R1:
    
  3. Loop over the rules.

    Add a loop to 'makeGraph' that loops over the rule vector. This loop identifies the 'from' end of possible edges in the graph. Print a line similar to the following for each rule:

    from rule R0: A() :- B()
    
    (The printing in these steps is used for the purpose of capturing your results from the lab session and should be removed for use in the project.)

    Compile and test. The output should look something like this:

    from rule R0: A() :- B()
    from rule R1: B() :- B(),A()
    R0:
    R1:
    
  4. Loop over the predicates in the rule body.

    Add a loop to 'makeGraph' that loops over the predicates in the body of the current rule. (You need to look at the names in the body to find dependencies.) Print a line similar to the following for each body predicate:

    from body predicate: B()
    

    Compile and test. The output should look something like this:

    from rule R0: A() :- B()
    from body predicate: B()
    from rule R1: B() :- B(),A()
    from body predicate: B()
    from body predicate: A()
    R0:
    R1:
    
  5. Loop over the rules again.

    Add another loop to 'makeGraph' to loop over the rules again. This loop identifies the 'to' end of possible edges in the graph. (You should now have three loops that are nested inside each other.) Print a line similar to the following for each rule:

    to rule R0: A() :- B()
    

    Compile and test. The output should look something like this:

    from rule R0: A() :- B()
    from body predicate: B()
    to rule R0: A() :- B()
    to rule R1: B() :- B(),A()
    from rule R1: B() :- B(),A()
    from body predicate: B()
    to rule R0: A() :- B()
    to rule R1: B() :- B(),A()
    from body predicate: A()
    to rule R0: A() :- B()
    to rule R1: B() :- B(),A()
    R0:
    R1:
    
  6. Find matching names.

    Where in the output from the previous step do you see names that match in a way that indicates an edge should be added to the graph?

    If a predicate name in the body of the 'from' rule matches the predicate name in the head of the 'to' rule, add an edge to the graph. Also print a line similar to the following for the purpose of capturing your lab session results.

    dependency found: (R0,R1)
    

    Compile and test. The output should look something like this:

    from rule R0: A() :- B()
    from body predicate: B()
    to rule R0: A() :- B()
    to rule R1: B() :- B(),A()
    dependency found: (R0,R1)
    from rule R1: B() :- B(),A()
    from body predicate: B()
    to rule R0: A() :- B()
    to rule R1: B() :- B(),A()
    dependency found: (R1,R1)
    from body predicate: A()
    to rule R0: A() :- B()
    dependency found: (R1,R0)
    to rule R1: B() :- B(),A()
    R0:R1
    R1:R0,R1
    
  7. Take a screenshot showing the output from testing your 'makeGraph' function.

  8. Test 'makeGraph' with other rules.

    Replace 'ruleNames' in your 'main' function with the code below. (You may also want to either comment or remove the print statements in the 'makeGraph' function.)

      pair<string,vector<string>> ruleNames[] = {
        { "Sibling", { "Sibling" } },
        { "Ancestor", { "Ancestor", "Parent" } },
        { "Ancestor", { "Parent" } },
      };
    

    Compile and test. The output should look something like this:

    R0:R0
    R1:R1,R2
    R2:
    

    Replace 'ruleNames' in your 'main' function again.

      pair<string,vector<string>> ruleNames[] = {
        { "A", { "B", "C" } },
        { "B", { "A", "D" } },
        { "B", { "B" } },
        { "E", { "F", "G" } },
        { "E", { "E", "F" } },
      };
    

    Compile and test. The output should look something like this:

    R0:R1,R2
    R1:R0
    R2:R1,R2
    R3:
    R4:R3,R4
    
  9. Submit your screenshots and a zip file containing the code you wrote during this session to Learning Suite.