dts: dtlib/edtlib: Add phandle and phandle+nums array types

Add two new type-checked property types 'phandles' and 'phandle-array'
to edtlib.

'phandles' is for pure lists of phandles, with no other data, like

    foo = < &bar &baz ... >

'phandle-array' is for lists of phandles and (possibly) numbers, like

    foo = < &bar 1 2 &baz 3 4 ... >

dt-schema also has the 'phandle-array' type.

Property.val (in edtlib) is set to an array of Device objects for the
'phandles' type.

For the 'phandle-array' type, no Property object is created. This type
is only used for type checking.

Also refactor how types that do not create a Property object
('phandle-array' and 'compound') are handled. Have _prop_val() return
None for them.

The new types are implemented with two new TYPE_PHANDLES and
TYPE_PHANDLES_AND_NUMS types at the dtlib level. There is also a new
Property.to_nodes() functions for fetching the Nodes for an array of
phandles, with type checking.

Signed-off-by: Ulf Magnusson <Ulf.Magnusson@nordicsemi.no>
diff --git a/scripts/dts/dtlib.py b/scripts/dts/dtlib.py
index 8dbdd28..e77f66a 100644
--- a/scripts/dts/dtlib.py
+++ b/scripts/dts/dtlib.py
@@ -1359,24 +1359,28 @@
       assignment. This is one of the following constants (with example
       assignments):
 
-        Assignment         | Property.type
-        -------------------+------------------------
-        foo;               | dtlib.TYPE_EMPTY
-        foo = []           | dtlib.TYPE_BYTES
-        foo = [01 02]      | dtlib.TYPE_BYTES
-        foo = /bits/ 8 <1> | dtlib.TYPE_BYTES
-        foo = <1>          | dtlib.TYPE_NUM
-        foo = <>           | dtlib.TYPE_NUMS
-        foo = <1 2 3>      | dtlib.TYPE_NUMS
-        foo = <1 2>, <3>   | dtlib.TYPE_NUMS
-        foo = "foo"        | dtlib.TYPE_STRING
-        foo = "foo", "bar" | dtlib.TYPE_STRINGS
-        foo = <&label>     | dtlib.TYPE_PHANDLE
-        foo = &label       | dtlib.TYPE_PATH
-        *Anything else*    | dtlib.TYPE_COMPOUND
+        Assignment                  | Property.type
+        ----------------------------+------------------------
+        foo;                        | dtlib.TYPE_EMPTY
+        foo = [];                   | dtlib.TYPE_BYTES
+        foo = [01 02];              | dtlib.TYPE_BYTES
+        foo = /bits/ 8 <1>;         | dtlib.TYPE_BYTES
+        foo = <1>;                  | dtlib.TYPE_NUM
+        foo = <>;                   | dtlib.TYPE_NUMS
+        foo = <1 2 3>;              | dtlib.TYPE_NUMS
+        foo = <1 2>, <3>;           | dtlib.TYPE_NUMS
+        foo = "foo";                | dtlib.TYPE_STRING
+        foo = "foo", "bar";         | dtlib.TYPE_STRINGS
+        foo = <&l>;                 | dtlib.TYPE_PHANDLE
+        foo = <&l1 &l2 &l3>;        | dtlib.TYPE_PHANDLES
+        foo = <&l1 &l2>, <&l3>;     | dtlib.TYPE_PHANDLES
+        foo = <&l1 1 2 &l2 3 4>;    | dtlib.TYPE_PHANDLES_AND_NUMS
+        foo = <&l1 1 2>, <&l2 3 4>; | dtlib.TYPE_PHANDLES_AND_NUMS
+        foo = &l;                   | dtlib.TYPE_PATH
+        *Anything else*             | dtlib.TYPE_COMPOUND
 
-      *Anything else* includes properties mixing (<&label>) and node path
-      (&label) references with other data.
+      *Anything else* includes properties mixing phandle (<&label>) and node
+      path (&label) references with other data.
 
       Data labels in the property value do not influence the type.
 
@@ -1550,6 +1554,34 @@
 
         return self.node.dt.phandle2node[int.from_bytes(self.value, "big")]
 
+    def to_nodes(self):
+        """
+        Returns a list with the Nodes the phandles in the property point to.
+
+        Raises DTError if the property value contains anything other than
+        phandles. All of the following are accepted:
+
+            foo = < >
+            foo = < &bar >;
+            foo = < &bar &baz ... >;
+            foo = < &bar ... >, < &baz ... >;
+        """
+        def type_ok():
+            if self.type in (TYPE_PHANDLE, TYPE_PHANDLES):
+                return True
+            # Also accept 'foo = < >;'
+            return self.type is TYPE_NUMS and not self.value
+
+        if not type_ok():
+            raise DTError("expected property '{0}' on {1} in {2} to be "
+                          "assigned with '{0} = < &foo &bar ... >;', not '{3}'"
+                          .format(self.name, self.node.path,
+                                  self.node.dt.filename, self))
+
+        return [self.node.dt.phandle2node[int.from_bytes(self.value[i:i + 4],
+                                                         "big")]
+                for i in range(0, len(self.value), 4)]
+
     def to_path(self):
         """
         Returns the Node referenced by the path stored in the property.
@@ -1617,6 +1649,13 @@
         if types == [_TYPE_UINT32, _REF_PHANDLE] and len(self.value) == 4:
             return TYPE_PHANDLE
 
+        if set(types) == {_TYPE_UINT32, _REF_PHANDLE}:
+            if len(self.value) == 4*types.count(_REF_PHANDLE):
+                # Array with just phandles in it
+                return TYPE_PHANDLES
+            # Array with both phandles and numbers
+            return TYPE_PHANDLES_AND_NUMS
+
         return TYPE_COMPOUND
 
     def __str__(self):
@@ -1765,7 +1804,9 @@
 TYPE_STRINGS = 5
 TYPE_PATH = 6
 TYPE_PHANDLE = 7
-TYPE_COMPOUND = 8
+TYPE_PHANDLES = 8
+TYPE_PHANDLES_AND_NUMS = 9
+TYPE_COMPOUND = 10
 
 
 def _check_is_bytes(data):
diff --git a/scripts/dts/edtlib.py b/scripts/dts/edtlib.py
index a6e6ca7..9959d14 100644
--- a/scripts/dts/edtlib.py
+++ b/scripts/dts/edtlib.py
@@ -29,7 +29,8 @@
 
 import yaml
 
-from dtlib import DT, DTError, to_num, to_nums, TYPE_EMPTY
+from dtlib import DT, DTError, to_num, to_nums, TYPE_EMPTY, TYPE_PHANDLE, \
+                  TYPE_PHANDLES_AND_NUMS
 
 # NOTE: testedtlib.py is the test suite for this library. It can be run
 # directly.
@@ -532,17 +533,11 @@
         if not prop_type:
             _err("'{}' in {} lacks 'type'".format(name, self.binding_path))
 
-        # "Dummy" type for properties like '...-gpios', so that we can require
-        # all entries in 'properties:' to have a 'type: ...'. It might make
-        # sense to have gpios in 'properties:' for other reasons, e.g. to set
-        # 'category: required'.
-        if prop_type == "compound":
-            return
-
         val = self._prop_val(name, prop_type,
                              options.get("category") == "optional")
         if val is None:
-            # 'category: optional' property that wasn't there
+            # 'category: optional' property that wasn't there, or a property
+            # type for which we store no data.
             return
 
         enum = options.get("enum")
@@ -616,6 +611,25 @@
         if prop_type == "phandle":
             return self.edt._node2dev[prop.to_node()]
 
+        if prop_type == "phandles":
+            return [self.edt._node2dev[node] for node in prop.to_nodes()]
+
+        if prop_type == "phandle-array":
+            # This property type only does a type check. No Property object is
+            # created for it.
+            if prop.type not in (TYPE_PHANDLE, TYPE_PHANDLES_AND_NUMS):
+                _err("expected property '{0}' in {1} in {2} to be assigned "
+                     "with '{0} = < &foo 1 2 ... &bar 3 4 ... >' (a mix of "
+                     "phandles and numbers), not '{3}'"
+                     .format(name, node.path, node.dt.filename, prop))
+            return None
+
+        if prop_type == "compound":
+            # Dummy type for properties like that don't fit any of the patterns
+            # above, so that we can require all entries in 'properties:' to
+            # have a 'type: ...'. No Property object is created for it.
+            return None
+
         _err("'{}' in 'properties:' in {} has unknown type '{}'"
              .format(name, self.binding_path, prop_type))
 
@@ -1007,7 +1021,9 @@
     Represents a property on a Device, as set in its DT node and with
     additional info from the 'properties:' section of the binding.
 
-    Only properties mentioned in 'properties:' get created.
+    Only properties mentioned in 'properties:' get created. Properties with
+    type 'phandle-array' or type 'compound' do not get Property instance. These
+    types only exist for type checking.
 
     These attributes are available on Property objects:
 
@@ -1023,8 +1039,12 @@
 
     val:
       The value of the property, with the format determined by the 'type:' key
-      from the binding. For 'type: phandle' properties, this is the pointed-to
-      Device instance.
+      from the binding.
+
+      For 'type: phandle' properties, this is the pointed-to Device instance.
+
+      For 'type: phandles' properties, this is a list of the pointed-to Device
+      instances.
 
     enum_index:
       The index of the property's value in the 'enum:' list in the binding, or
diff --git a/scripts/dts/test-bindings/props.yaml b/scripts/dts/test-bindings/props.yaml
index e248c7a..bf45e30 100644
--- a/scripts/dts/test-bindings/props.yaml
+++ b/scripts/dts/test-bindings/props.yaml
@@ -33,3 +33,9 @@
 
     phandle-ref:
         type: phandle
+
+    phandle-refs:
+        type: phandles
+
+    phandle-refs-and-vals:
+        type: phandle-array
diff --git a/scripts/dts/test.dts b/scripts/dts/test.dts
index e90a615..5cb4e9d 100644
--- a/scripts/dts/test.dts
+++ b/scripts/dts/test.dts
@@ -297,12 +297,17 @@
 		string = "foo";
 		string-array = "foo", "bar", "baz";
 		phandle-ref = < &{/props/node} >;
+		phandle-refs = < &{/props/node} &{/props/node2} >;
+		phandle-refs-and-vals = < &{/props/node} 1 &{/props/node2} 2 >;
 		// Does not appear in the binding, so won't create an entry in
 		// Device.props
 		not-speced = <0>;
 
 		node {
 		};
+
+		node2 {
+		};
 	};
 
 	//
diff --git a/scripts/dts/testdtlib.py b/scripts/dts/testdtlib.py
index 1fd2648..029b865 100755
--- a/scripts/dts/testdtlib.py
+++ b/scripts/dts/testdtlib.py
@@ -1451,13 +1451,17 @@
 	nums4 = < 1 2 >, < 3 >, < 4 >;
 	string = "foo";
 	strings = "foo", "bar";
-	phandle1 = < &node >;
-	phandle2 = < &{/node} >;
 	path1 = &node;
 	path2 = &{/node};
+	phandle1 = < &node >;
+	phandle2 = < &{/node} >;
+	phandles1 = < &node &node >;
+	phandles2 = < &node >, < &node >;
+	phandle-and-nums-1 = < &node 1 >;
+	phandle-and-nums-2 = < &node 1 2 &node 3 4 >;
+	phandle-and-nums-3 = < &node 1 2 >, < &node 3 4 >;
 	compound1 = < 1 >, [ 02 ];
 	compound2 = "foo", < >;
-	compound3 = < 1 &{/node} 2>;
 
 	node: node {
 	};
@@ -1479,11 +1483,15 @@
     verify_type("strings", dtlib.TYPE_STRINGS)
     verify_type("phandle1", dtlib.TYPE_PHANDLE)
     verify_type("phandle2", dtlib.TYPE_PHANDLE)
+    verify_type("phandles1", dtlib.TYPE_PHANDLES)
+    verify_type("phandles2", dtlib.TYPE_PHANDLES)
+    verify_type("phandle-and-nums-1", dtlib.TYPE_PHANDLES_AND_NUMS)
+    verify_type("phandle-and-nums-2", dtlib.TYPE_PHANDLES_AND_NUMS)
+    verify_type("phandle-and-nums-3", dtlib.TYPE_PHANDLES_AND_NUMS)
     verify_type("path1", dtlib.TYPE_PATH)
     verify_type("path2", dtlib.TYPE_PATH)
     verify_type("compound1", dtlib.TYPE_COMPOUND)
     verify_type("compound2", dtlib.TYPE_COMPOUND)
-    verify_type("compound3", dtlib.TYPE_COMPOUND)
 
     #
     # Test Property.to_{num,nums,string,strings,node}()
@@ -1511,6 +1519,8 @@
 	strings = "foo", "bar", "baz";
 	invalid_strings = "foo", "\xff", "bar";
 	ref = <&{/target}>;
+	refs = <&{/target} &{/target2}>;
+	refs2 = <&{/target}>, <&{/target2}>;
 	path = &{/target};
 	manualpath = "/target";
 	missingpath = "/missing";
@@ -1518,6 +1528,9 @@
 	target {
 		phandle = < 100 >;
 	};
+
+	target2 {
+	};
 };
 """)
 
@@ -1717,6 +1730,38 @@
     verify_to_node_error("u", "expected property 'u' on / in .tmp.dts to be assigned with 'u = < &foo >;', not 'u = < 0x1 >;'")
     verify_to_node_error("string", "expected property 'string' on / in .tmp.dts to be assigned with 'string = < &foo >;', not 'string = \"foo\\tbar baz\";'")
 
+    # Test Property.to_nodes()
+
+    def verify_to_nodes(prop, paths):
+        try:
+            actual = [node.path for node in dt.root.props[prop].to_nodes()]
+        except dtlib.DTError as e:
+            fail("failed to convert '{}' to nodes: {}".format(prop, e))
+
+        if actual != paths:
+            fail("expected {} to point to the paths {}, pointed to {}"
+                 .format(prop, paths, actual))
+
+    def verify_to_nodes_error(prop, msg):
+        prefix = "expected converting '{}' to a nodes to generate the error " \
+                 "'{}', generated".format(prop, msg)
+        try:
+            dt.root.props[prop].to_nodes()
+            fail(prefix + " no error")
+        except dtlib.DTError as e:
+            if str(e) != msg:
+                fail("{} the error '{}'".format(prefix, e))
+        except Exception as e:
+            fail("{} the non-DTError '{}'".format(prefix, e))
+
+    verify_to_nodes("zero", [])
+    verify_to_nodes("ref", ["/target"])
+    verify_to_nodes("refs", ["/target", "/target2"])
+    verify_to_nodes("refs2", ["/target", "/target2"])
+
+    verify_to_nodes_error("u", "expected property 'u' on / in .tmp.dts to be assigned with 'u = < &foo &bar ... >;', not 'u = < 0x1 >;'")
+    verify_to_nodes_error("string", "expected property 'string' on / in .tmp.dts to be assigned with 'string = < &foo &bar ... >;', not 'string = \"foo\\tbar baz\";'")
+
     # Test Property.to_path()
 
     def verify_to_path(prop, path):
diff --git a/scripts/dts/testedtlib.py b/scripts/dts/testedtlib.py
index 449aae7..40d1cfc 100755
--- a/scripts/dts/testedtlib.py
+++ b/scripts/dts/testedtlib.py
@@ -120,7 +120,7 @@
     #
 
     verify_streq(edt.get_dev("/props").props,
-                 r"{'compatible': <Property, name: compatible, value: ['props']>, 'nonexistent-boolean': <Property, name: nonexistent-boolean, value: False>, 'existent-boolean': <Property, name: existent-boolean, value: True>, 'int': <Property, name: int, value: 1>, 'array': <Property, name: array, value: [1, 2, 3]>, 'uint8-array': <Property, name: uint8-array, value: b'\x124'>, 'string': <Property, name: string, value: 'foo'>, 'string-array': <Property, name: string-array, value: ['foo', 'bar', 'baz']>, 'phandle-ref': <Property, name: phandle-ref, value: <Device /props/node in 'test.dts', no binding>>}")
+                 r"{'compatible': <Property, name: compatible, value: ['props']>, 'nonexistent-boolean': <Property, name: nonexistent-boolean, value: False>, 'existent-boolean': <Property, name: existent-boolean, value: True>, 'int': <Property, name: int, value: 1>, 'array': <Property, name: array, value: [1, 2, 3]>, 'uint8-array': <Property, name: uint8-array, value: b'\x124'>, 'string': <Property, name: string, value: 'foo'>, 'string-array': <Property, name: string-array, value: ['foo', 'bar', 'baz']>, 'phandle-ref': <Property, name: phandle-ref, value: <Device /props/node in 'test.dts', no binding>>, 'phandle-refs': <Property, name: phandle-refs, value: [<Device /props/node in 'test.dts', no binding>, <Device /props/node2 in 'test.dts', no binding>]>}")
 
     #
     # Test having multiple directories with bindings, with a different .dts file