edtlib: support inferring binding from node content

Clean up of devicetree tooling removed generation of information
present in devicetree in nodes that have no compatible, or for extra
properties not defined by a binding.  Discussion proposed that these
properties should be allowed, but only in a defined node /zephyr,user.
For that node infer bindings based on the presence of properties.

Signed-off-by: Peter Bigot <peter.bigot@nordicsemi.no>
diff --git a/scripts/dts/edtlib.py b/scripts/dts/edtlib.py
index fc6c4d5..26c7a12 100644
--- a/scripts/dts/edtlib.py
+++ b/scripts/dts/edtlib.py
@@ -83,8 +83,9 @@
 except ImportError:
     from yaml import Loader
 
-from dtlib import DT, DTError, to_num, to_nums, TYPE_EMPTY, TYPE_NUMS, \
-                  TYPE_PHANDLE, TYPE_PHANDLES_AND_NUMS
+from dtlib import DT, DTError, to_num, to_nums, TYPE_EMPTY, TYPE_BYTES, \
+                  TYPE_NUM, TYPE_NUMS, TYPE_STRING, TYPE_STRINGS, \
+                  TYPE_PHANDLE, TYPE_PHANDLES, TYPE_PHANDLES_AND_NUMS
 from grutils import Graph
 
 
@@ -157,9 +158,9 @@
     def __init__(self, dts, bindings_dirs, warn_file=None,
                  warn_reg_unit_address_mismatch=True,
                  default_prop_types=True,
-                 support_fixed_partitions_on_any_bus=True):
-        """
-        EDT constructor. This is the top-level entry point to the library.
+                 support_fixed_partitions_on_any_bus=True,
+                 infer_binding_for_paths=None):
+        """EDT constructor. This is the top-level entry point to the library.
 
         dts:
           Path to devicetree .dts file
@@ -184,6 +185,12 @@
           If True, set the Node.bus for 'fixed-partitions' compatible nodes
           to None.  This allows 'fixed-partitions' binding to match regardless
           of the bus the 'fixed-partition' is under.
+
+        infer_binding_for_paths (default: None):
+          An iterable of devicetree paths identifying nodes for which bindings
+          should be inferred from the node content.  (Child nodes are not
+          processed.)  Pass none if no nodes should support inferred bindings.
+
         """
         # Do this indirection with None in case sys.stderr is
         # deliberately overridden. We'll only hold on to this file
@@ -193,6 +200,7 @@
         self._warn_reg_unit_address_mismatch = warn_reg_unit_address_mismatch
         self._default_prop_types = default_prop_types
         self._fixed_partitions_no_bus = support_fixed_partitions_on_any_bus
+        self._infer_binding_for_paths = set(infer_binding_for_paths or [])
 
         self.dts_path = dts
         self.bindings_dirs = bindings_dirs
@@ -957,6 +965,10 @@
         # initialized, which is guaranteed by going through the nodes in
         # node_iter() order.
 
+        if self.path in self.edt._infer_binding_for_paths:
+            self._binding_from_properties()
+            return
+
         if self.compats:
             on_bus = self.on_bus
 
@@ -984,6 +996,43 @@
         # No binding found
         self._binding = self.binding_path = self.matching_compat = None
 
+    def _binding_from_properties(self):
+        # Returns a binding synthesized from the properties in the node.
+
+        if self.compats:
+            _err(f"compatible in node with inferred binding: {self.path}")
+
+        self._binding = OrderedDict()
+        self.matching_compat = self.path.split('/')[-1]
+        self.compats = [self.matching_compat]
+        self.binding_path = None
+
+        properties = OrderedDict()
+        self._binding["properties"] = properties
+        for name, prop in self._node.props.items():
+            pp = OrderedDict()
+            properties[name] = pp
+            if prop.type == TYPE_EMPTY:
+                pp["type"] = "boolean"
+            elif prop.type == TYPE_BYTES:
+                pp["type"] = "uint8-array"
+            elif prop.type == TYPE_NUM:
+                pp["type"] = "int"
+            elif prop.type == TYPE_NUMS:
+                pp["type"] = "array"
+            elif prop.type == TYPE_STRING:
+                pp["type"] = "string"
+            elif prop.type == TYPE_STRINGS:
+                pp["type"] = "string-array"
+            elif prop.type == TYPE_PHANDLE:
+                pp["type"] = "phandle"
+            elif prop.type == TYPE_PHANDLES:
+                pp["type"] = "phandles"
+            elif prop.type == TYPE_PHANDLES_AND_NUMS:
+                pp["type"] = "phandle-array"
+            else:
+                _err(f"cannot infer binding from property: {prop}")
+
     def _binding_from_parent(self):
         # Returns the binding from 'child-binding:' in the parent node's
         # binding (or from the legacy 'sub-node:' key), or None if missing
diff --git a/scripts/dts/test.dts b/scripts/dts/test.dts
index 695f925..64330d1 100644
--- a/scripts/dts/test.dts
+++ b/scripts/dts/test.dts
@@ -362,6 +362,22 @@
 	};
 
 	//
+	// zephyr,user binding inference
+	//
+
+	zephyr,user {
+		boolean;
+		bytes = [81 82 83];
+		number = <23>;
+		numbers = <1>, <2>, <3>;
+		string = "text";
+		strings = "a", "b", "c";
+		handle = <&{/props/ctrl-1}>;
+		phandles = <&{/props/ctrl-1}>, <&{/props/ctrl-2}>;
+		phandle-array-foos = <&{/props/ctrl-2} 1 2>;
+	};
+
+	//
 	// For testing that neither 'include: [foo.yaml, bar.yaml]' nor
 	// 'include: [bar.yaml, foo.yaml]' causes errors when one of the files
 	// has 'required: true' and the other 'required: false'
diff --git a/scripts/dts/testedtlib.py b/scripts/dts/testedtlib.py
index 1ef332e..88236ae 100755
--- a/scripts/dts/testedtlib.py
+++ b/scripts/dts/testedtlib.py
@@ -216,6 +216,18 @@
                  r"OrderedDict([('int', <Property, name: int, type: int, value: 123>), ('array', <Property, name: array, type: array, value: [1, 2, 3]>), ('uint8-array', <Property, name: uint8-array, type: uint8-array, value: b'\x89\xab\xcd'>), ('string', <Property, name: string, type: string, value: 'hello'>), ('string-array', <Property, name: string-array, type: string-array, value: ['hello', 'there']>), ('default-not-used', <Property, name: default-not-used, type: int, value: 234>)])")
 
     #
+    # Test binding inference
+    #
+
+    verify_streq(edt.get_node("/zephyr,user").props, r"OrderedDict()")
+
+    edt = edtlib.EDT("test.dts", ["test-bindings"], warnings,
+                     infer_binding_for_paths=["/zephyr,user"])
+
+    verify_streq(edt.get_node("/zephyr,user").props,
+                 r"OrderedDict([('boolean', <Property, name: boolean, type: boolean, value: True>), ('bytes', <Property, name: bytes, type: uint8-array, value: b'\x81\x82\x83'>), ('number', <Property, name: number, type: int, value: 23>), ('numbers', <Property, name: numbers, type: array, value: [1, 2, 3]>), ('string', <Property, name: string, type: string, value: 'text'>), ('strings', <Property, name: strings, type: string-array, value: ['a', 'b', 'c']>), ('handle', <Property, name: handle, type: phandle, value: <Node /props/ctrl-1 in 'test.dts', binding test-bindings/phandle-array-controller-1.yaml>>), ('phandles', <Property, name: phandles, type: phandles, value: [<Node /props/ctrl-1 in 'test.dts', binding test-bindings/phandle-array-controller-1.yaml>, <Node /props/ctrl-2 in 'test.dts', binding test-bindings/phandle-array-controller-2.yaml>]>), ('phandle-array-foos', <Property, name: phandle-array-foos, type: phandle-array, value: [<ControllerAndData, controller: <Node /props/ctrl-2 in 'test.dts', binding test-bindings/phandle-array-controller-2.yaml>, data: OrderedDict([('one', 1), ('two', 2)])>]>)])")
+
+    #
     # Test having multiple directories with bindings, with a different .dts file
     #