diff --git a/scripts/dts/edtlib.py b/scripts/dts/edtlib.py index c403806e6a0..aace58b680f 100644 --- a/scripts/dts/edtlib.py +++ b/scripts/dts/edtlib.py @@ -36,6 +36,7 @@ except ImportError: from dtlib import DT, DTError, to_num, to_nums, TYPE_EMPTY, TYPE_NUMS, \ TYPE_PHANDLE, TYPE_PHANDLES_AND_NUMS +from grutils import Graph # NOTE: testedtlib.py is the test suite for this library. It can be run # directly as a script: @@ -127,6 +128,8 @@ class EDT: self._init_compat2binding(bindings_dirs) self._init_nodes() + self._define_order() + def get_node(self, path): """ Returns the Node at the DT path or alias 'path'. Raises EDTError if the @@ -158,6 +161,68 @@ class EDT: return "".format( self.dts_path, self.bindings_dirs) + def required_by(self, node): + """ + Returns a list of Nodes that have direct dependencies on 'node'. + """ + return self._graph.required_by(node) + + def depends_on(self, node): + """ + Returns a list of Nodes on which 'node' directly depends. + """ + return self._graph.depends_on(node) + + def scc_order(self): + """ + Returns a list of lists of Nodes where all elements of each list + depend on each other, and the Nodes in any list do not depend + on any Node in a subsequent list. Each list defines a Strongly + Connected Component (SCC) of the graph. + + For an acyclic graph each list will be a singleton. Cycles + will be represented by lists with multiple nodes. Cycles are + not expected to be present in devicetree graphs. + """ + try: + return self._graph.scc_order() + except Exception as e: + raise EDTError(e) + + def _define_order(self): + # Constructs a graph of dependencies between Node instances, + # then calculates a partial order over the dependencies. The + # algorithm supports detecting dependency loops. + + self._graph = Graph() + + for node in self.nodes: + # A Node always depends on its parent. + for child in node.children.values(): + self._graph.add_edge(child, node) + + # A Node depends on any Nodes present in 'phandle', + # 'phandles', or 'phandle-array' property values. + for prop in node.props.values(): + if prop.type == 'phandle': + self._graph.add_edge(node, prop.val) + elif prop.type == 'phandles': + for phandle_node in prop.val: + self._graph.add_edge(node, phandle_node) + elif prop.type == 'phandle-array': + for cd in prop.val: + self._graph.add_edge(node, cd.controller) + + # A Node depends on whatever supports the interrupts it + # generates. + for intr in node.interrupts: + self._graph.add_edge(node, intr.controller) + + # Calculate an order that ensures no node is before any node + # it depends on. This sets the dep_ordinal field in each + # Node. + self.scc_order() + def _init_compat2binding(self, bindings_dirs): # Creates self._compat2binding. This is a dictionary that maps # (, ) tuples (both strings) to (, ) @@ -583,6 +648,13 @@ class Node: A dictionary with the Node instances for the devicetree children of the node, indexed by name + dep_ordinal: + A non-negative integer value such that the value for a Node is + less than the value for all Nodes that depend on it. + + The ordinal is defined for all Nodes including those that are not + 'enabled', and is unique among nodes in its EDT 'nodes' list. + enabled: True unless the node has 'status = "disabled"' diff --git a/scripts/dts/gen_defines.py b/scripts/dts/gen_defines.py index 08873700bc3..82e45a95364 100755 --- a/scripts/dts/gen_defines.py +++ b/scripts/dts/gen_defines.py @@ -51,6 +51,15 @@ Directories with bindings: args.dts, ", ".join(map(relativize, args.bindings_dirs)) ), blank_before=False) + out_comment("Nodes in dependency order (ordinal : path):") + for scc in edt.scc_order(): + if len(scc) > 1: + err("Cycle in devicetree involving: {}".format( + ", ".join([n.path for n in scc]))) + node = scc[0] + out_comment("{} : {}".format(node.dep_ordinal, node.path), + blank_before = False) + active_compats = set() for node in edt.nodes: @@ -60,6 +69,20 @@ Directories with bindings: if node.matching_compat == "fixed-partitions": continue + requires_text = "" + if edt.depends_on(node): + requires_text = "Requires:\n" + for depends in edt.depends_on(node): + requires_text += " {} {}\n".format(depends.dep_ordinal, depends.path) + requires_text += "\n" + + supports_text = "" + if edt.required_by(node): + supports_text = "Supports:\n" + for required in edt.required_by(node): + supports_text += " {} {}\n".format(required.dep_ordinal, required.path) + supports_text += "\n" + out_comment("""\ Devicetree node: {} @@ -67,9 +90,12 @@ Devicetree node: Binding (compatible = {}): {} -Description: +Dependency Ordinal: {} + +{}{}Description: {}""".format( node.path, node.matching_compat, relativize(node.binding_path), + node.dep_ordinal, requires_text, supports_text, # Indent description by two spaces "\n".join(" " + line for line in node.description.splitlines()) )) diff --git a/scripts/dts/grutils.py b/scripts/dts/grutils.py new file mode 100644 index 00000000000..b88bb753f9f --- /dev/null +++ b/scripts/dts/grutils.py @@ -0,0 +1,142 @@ +# Copyright 2009-2013, 2019 Peter A. Bigot +# +# SPDX-License-Identifier: Apache-2.0 + +# This implementation is derived from the one in +# [PyXB](https://github.com/pabigot/pyxb), stripped down and modified +# specifically to manage edtlib Node instances. + +import collections + +from operator import attrgetter + +class Graph: + """ + Represent a directed graph with edtlib Node objects as nodes. + + This is used to determine order dependencies among nodes in a + devicetree. An edge from C{source} to C{target} indicates that + some aspect of C{source} requires that some aspect of C{target} + already be available. + """ + + def __init__ (self, root=None): + self.__roots = None + if root is not None: + self.__roots = {root} + self.__edge_map = collections.defaultdict(set) + self.__reverse_map = collections.defaultdict(set) + self.__nodes = set() + + def add_edge (self, source, target): + """ + Add a directed edge from the C{source} to the C{target}. + + The nodes are added to the graph if necessary. + """ + self.__edge_map[source].add(target) + if source != target: + self.__reverse_map[target].add(source) + self.__nodes.add(source) + self.__nodes.add(target) + + def roots (self): + """ + Return the set of nodes calculated to be roots (i.e., those + that have no incoming edges). + + This caches the roots calculated in a previous invocation. + + @rtype: C{set} + """ + if not self.__roots: + self.__roots = set() + for n in self.__nodes: + if n not in self.__reverse_map: + self.__roots.add(n) + return self.__roots + + def _tarjan (self): + # Execute Tarjan's algorithm on the graph. + # + # Tarjan's algorithm + # (http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm) + # computes the strongly-connected components + # (http://en.wikipedia.org/wiki/Strongly_connected_component) + # of the graph: i.e., the sets of nodes that form a minimal + # closed set under edge transition. In essence, the loops. + # We use this to detect groups of components that have a + # dependency cycle, and to impose a total order on components + # based on dependencies. + + self.__stack = [] + self.__scc_order = [] + self.__index = 0 + self.__tarjan_index = {} + self.__tarjan_low_link = {} + for v in self.__nodes: + self.__tarjan_index[v] = None + roots = self.roots() + if self.__nodes and not roots: + raise Exception('TARJAN: No roots found in graph with {} nodes'.format(len(self.__nodes))) + for r in sorted(roots, key=attrgetter('path')): + self._tarjan_root(r) + + # Assign ordinals for edtlib + ordinal = 0 + for scc in self.__scc_order: + # Zephyr customization: devicetree Node graphs should have + # no loops, so all SCCs should be singletons. That may + # change in the future, but for now we only give an + # ordinal to singletons. + if len(scc) == 1: + scc[0].dep_ordinal = ordinal + ordinal += 1 + + def _tarjan_root (self, v): + # Do the work of Tarjan's algorithm for a given root node. + + if self.__tarjan_index.get(v) is not None: + # "Root" was already reached. + return + self.__tarjan_index[v] = self.__tarjan_low_link[v] = self.__index + self.__index += 1 + self.__stack.append(v) + source = v + for target in sorted(self.__edge_map[source], key=attrgetter('path')): + if self.__tarjan_index[target] is None: + self._tarjan_root(target) + self.__tarjan_low_link[v] = min(self.__tarjan_low_link[v], self.__tarjan_low_link[target]) + elif target in self.__stack: + self.__tarjan_low_link[v] = min(self.__tarjan_low_link[v], self.__tarjan_low_link[target]) + + if self.__tarjan_low_link[v] == self.__tarjan_index[v]: + scc = [] + while True: + scc.append(self.__stack.pop()) + if v == scc[-1]: + break + self.__scc_order.append(scc) + + def scc_order (self): + """Return the strongly-connected components in order. + + The data structure is a list, in dependency order, of strongly + connected components (which can be single nodes). Appearance + of a node in a set earlier in the list indicates that it has + no dependencies on any node that appears in a subsequent set. + This order is preferred over a depth-first-search order for + code generation, since it detects loops. + """ + if not self.__scc_order: + self._tarjan() + return self.__scc_order + __scc_order = None + + def depends_on (self, node): + """Get the nodes that 'node' directly depends on.""" + return sorted(self.__edge_map[node], key=attrgetter('path')) + + def required_by (self, node): + """Get the nodes that directly depend on 'node'.""" + return sorted(self.__reverse_map[node], key=attrgetter('path')) diff --git a/scripts/dts/testedtlib.py b/scripts/dts/testedtlib.py index 038acddfeab..8b19b0d50d3 100755 --- a/scripts/dts/testedtlib.py +++ b/scripts/dts/testedtlib.py @@ -212,6 +212,24 @@ warning: "#cells:" in test-bindings/deprecated.yaml is deprecated and will be re "test-bindings-2/multidir.yaml") + # + # Test dependency relations + # + + #for node in edt.nodes: + # print("{}: {}".format(node.path, node.ordinal)) + # for before in edt.depends_on(node): + # print(" Requires: {} {}".format(before.ordinal, before.path)) + # for after in edt.required_by(node): + # print(" Supports: {} {}".format(after.ordinal, after.path)) + + verify_eq(edt.get_node("/").ordinal, 0) + verify_eq(edt.get_node("/in-dir-1").ordinal, 1) + if edt.get_node("/") not in edt.depends_on(edt.get_node("/in-dir-1")): + fail("/ depends-on /in-dir-1") + if edt.get_node("/in-dir-1") not in edt.required_by(edt.get_node("/")): + fail("/in-dir-1 required-by /") + print("all tests passed")