[lldb] Reland: Add Pythonic API to SBStructuredData extension (#156771)

* Adds `dynamic` property to automatically convert `SBStructuredData`
instances to the associated Python type (`str`, `int`, `float`, `bool`,
`NoneType`, etc)
* Implements `__getitem__` for Pythonic array and dictionary
subscripting
  * Subscripting return the result of the `dynamic` property
* Updates `__iter__` to support dictionary instances (supporting `for`
loops)
* Adds `__str__`, `__int__`, and `__float__`

With these changes, these two expressions are equal:

```py
data["name"] == data.GetValueForKey("name").GetStringValue(1024)
```

**Note**: Unlike the original commit (#155061), this re-commit removes
the `__bool__` implementation, which broke crashlog. Somewhere in the
crashlog execution, it depends on `__bool__` meaning only `IsValid()`.

Additionally did some cleanup in TestStructuredDataAPI.py.
diff --git a/lldb/bindings/interface/SBStructuredDataExtensions.i b/lldb/bindings/interface/SBStructuredDataExtensions.i
index ca3d096..af76cfc 100644
--- a/lldb/bindings/interface/SBStructuredDataExtensions.i
+++ b/lldb/bindings/interface/SBStructuredDataExtensions.i
@@ -3,16 +3,119 @@
 %extend lldb::SBStructuredData {
 #ifdef SWIGPYTHON
     %pythoncode%{
-    def __int__(self):
-      return self.GetSignedInteger()
-
     def __len__(self):
       '''Return the number of element in a lldb.SBStructuredData object.'''
       return self.GetSize()
 
     def __iter__(self):
         '''Iterate over all the elements in a lldb.SBStructuredData object.'''
-        return lldb_iter(self, 'GetSize', 'GetItemAtIndex')
+        data_type = self.GetType()
+        if data_type == eStructuredDataTypeArray:
+            for i in range(self.GetSize()):
+                yield self.GetItemAtIndex(i).dynamic
+            return
+        elif data_type == eStructuredDataTypeDictionary:
+            keys = SBStringList()
+            self.GetKeys(keys)
+            return iter(keys)
+        else:
+            raise TypeError(f"cannot iterate {self.type_name(data_type)} type")
+
+    def __getitem__(self, key):
+        data_type = self.GetType()
+        if data_type == eStructuredDataTypeArray:
+            if not isinstance(key, int):
+                raise TypeError("subscript index must be an integer")
+            count = len(self)
+            if -count <= key < count:
+                key %= count
+                return self.GetItemAtIndex(key).dynamic
+            raise IndexError("index out of range")
+        elif data_type == eStructuredDataTypeDictionary:
+            if not isinstance(key, str):
+                raise TypeError("subscript key must be a string")
+            return self.GetValueForKey(key).dynamic
+        else:
+            raise TypeError(f"cannot subscript {self.type_name(data_type)} type")
+
+    def __str__(self):
+        data_type = self.GetType()
+        if data_type in (
+            eStructuredDataTypeString,
+            eStructuredDataTypeInteger,
+            eStructuredDataTypeSignedInteger,
+            eStructuredDataTypeFloat,
+        ):
+            return str(self.dynamic)
+        else:
+            return repr(self)
+
+    def __int__(self):
+        data_type = self.GetType()
+        if data_type in (
+            eStructuredDataTypeInteger,
+            eStructuredDataTypeSignedInteger,
+        ):
+            return self.dynamic
+        else:
+            raise TypeError(f"cannot convert {self.type_name(data_type)} to int")
+
+    def __float__(self):
+        data_type = self.GetType()
+        if data_type in (
+            eStructuredDataTypeFloat,
+            eStructuredDataTypeInteger,
+            eStructuredDataTypeSignedInteger,
+        ):
+            return float(self.dynamic)
+        else:
+            raise TypeError(f"cannot convert {self.type_name(data_type)} to float")
+
+    @property
+    def dynamic(self):
+        data_type = self.GetType()
+        if data_type == eStructuredDataTypeNull:
+            return None
+        elif data_type == eStructuredDataTypeBoolean:
+            return self.GetBooleanValue()
+        elif data_type == eStructuredDataTypeInteger:
+            return self.GetUnsignedIntegerValue()
+        elif data_type == eStructuredDataTypeSignedInteger:
+            return self.GetSignedIntegerValue()
+        elif data_type == eStructuredDataTypeFloat:
+            return self.GetFloatValue()
+        elif data_type == eStructuredDataTypeString:
+            size = len(self) or 1023
+            return self.GetStringValue(size + 1)
+        elif data_type == eStructuredDataTypeGeneric:
+            return self.GetGenericValue()
+        else:
+            return self
+
+    @staticmethod
+    def type_name(t):
+        if t == eStructuredDataTypeNull:
+            return "null"
+        elif t == eStructuredDataTypeBoolean:
+            return "boolean"
+        elif t == eStructuredDataTypeInteger:
+            return "integer"
+        elif t == eStructuredDataTypeSignedInteger:
+            return "integer"
+        elif t == eStructuredDataTypeFloat:
+            return "float"
+        elif t == eStructuredDataTypeString:
+            return "string"
+        elif t == eStructuredDataTypeArray:
+            return "array"
+        elif t == eStructuredDataTypeDictionary:
+            return "dictionary"
+        elif t == eStructuredDataTypeGeneric:
+            return "generic"
+        elif t == eStructuredDataTypeInvalid:
+            return "invalid"
+        else:
+            raise TypeError(f"unknown structured data type: {t}")
     %}
 #endif
 }
diff --git a/lldb/test/API/python_api/sbstructureddata/TestStructuredDataAPI.py b/lldb/test/API/python_api/sbstructureddata/TestStructuredDataAPI.py
index 99f88d3..b12f4da 100644
--- a/lldb/test/API/python_api/sbstructureddata/TestStructuredDataAPI.py
+++ b/lldb/test/API/python_api/sbstructureddata/TestStructuredDataAPI.py
@@ -48,10 +48,11 @@
         s.Clear()
         error = example.GetDescription(s)
         self.assertSuccess(error, "GetDescription works")
+        # Ensure str() doesn't raise an exception.
+        self.assertTrue(str(example))
         if not "key_float" in s.GetData():
             self.fail("FAILED: could not find key_float in description output")
 
-        dict_struct = lldb.SBStructuredData()
         dict_struct = example.GetValueForKey("key_dict")
 
         # Tests for dictionary data type
@@ -113,18 +114,22 @@
         self.assertSuccess(example.SetFromJSON("1"))
         self.assertEqual(example.GetType(), lldb.eStructuredDataTypeInteger)
         self.assertEqual(example.GetIntegerValue(), 1)
+        self.assertEqual(int(example), 1)
 
         self.assertSuccess(example.SetFromJSON("4.19"))
         self.assertEqual(example.GetType(), lldb.eStructuredDataTypeFloat)
         self.assertEqual(example.GetFloatValue(), 4.19)
+        self.assertEqual(float(example), 4.19)
 
         self.assertSuccess(example.SetFromJSON('"Bonjour, 123!"'))
         self.assertEqual(example.GetType(), lldb.eStructuredDataTypeString)
         self.assertEqual(example.GetStringValue(42), "Bonjour, 123!")
+        self.assertEqual(str(example), "Bonjour, 123!")
 
         self.assertSuccess(example.SetFromJSON("true"))
         self.assertEqual(example.GetType(), lldb.eStructuredDataTypeBoolean)
         self.assertTrue(example.GetBooleanValue())
+        self.assertTrue(example)
 
         self.assertSuccess(example.SetFromJSON("null"))
         self.assertEqual(example.GetType(), lldb.eStructuredDataTypeNull)
@@ -187,38 +192,35 @@
         self.assertEqual(sb_data, example_arr)
 
     def invalid_struct_test(self, example):
-        invalid_struct = lldb.SBStructuredData()
         invalid_struct = example.GetValueForKey("invalid_key")
         if invalid_struct.IsValid():
             self.fail("An invalid object should have been returned")
 
         # Check Type API
-        if not invalid_struct.GetType() == lldb.eStructuredDataTypeInvalid:
+        if invalid_struct.GetType() != lldb.eStructuredDataTypeInvalid:
             self.fail("Wrong type returned: " + str(invalid_struct.GetType()))
 
     def dictionary_struct_test(self, example):
         # Check API returning a valid SBStructuredData of 'dictionary' type
-        dict_struct = lldb.SBStructuredData()
         dict_struct = example.GetValueForKey("key_dict")
         if not dict_struct.IsValid():
             self.fail("A valid object should have been returned")
 
         # Check Type API
-        if not dict_struct.GetType() == lldb.eStructuredDataTypeDictionary:
+        if dict_struct.GetType() != lldb.eStructuredDataTypeDictionary:
             self.fail("Wrong type returned: " + str(dict_struct.GetType()))
 
         # Check Size API for 'dictionary' type
-        if not dict_struct.GetSize() == 6:
+        if dict_struct.GetSize() != 6:
             self.fail("Wrong no of elements returned: " + str(dict_struct.GetSize()))
 
     def string_struct_test(self, dict_struct):
-        string_struct = lldb.SBStructuredData()
         string_struct = dict_struct.GetValueForKey("key_string")
         if not string_struct.IsValid():
             self.fail("A valid object should have been returned")
 
         # Check Type API
-        if not string_struct.GetType() == lldb.eStructuredDataTypeString:
+        if string_struct.GetType() != lldb.eStructuredDataTypeString:
             self.fail("Wrong type returned: " + str(string_struct.GetType()))
 
         # Check API returning 'string' value
@@ -238,18 +240,17 @@
         # Check a valid SBStructuredData containing an unsigned integer.
         # We intentionally make this larger than what an int64_t can hold but
         # still small enough to fit a uint64_t
-        uint_struct = lldb.SBStructuredData()
         uint_struct = dict_struct.GetValueForKey("key_uint")
         if not uint_struct.IsValid():
             self.fail("A valid object should have been returned")
 
         # Check Type API
-        if not uint_struct.GetType() == lldb.eStructuredDataTypeInteger:
+        if uint_struct.GetType() != lldb.eStructuredDataTypeInteger:
             self.fail("Wrong type returned: " + str(uint_struct.GetType()))
 
         # Check API returning unsigned integer value
         output = uint_struct.GetUnsignedIntegerValue()
-        if not output == 0xFFFFFFFF00000000:
+        if output != 0xFFFFFFFF00000000:
             self.fail("wrong output: " + str(output))
 
         # Calling wrong API on a SBStructuredData
@@ -262,18 +263,17 @@
         # Check a valid SBStructuredData containing an signed integer.
         # We intentionally make this smaller than what an uint64_t can hold but
         # still small enough to fit a int64_t
-        sint_struct = lldb.SBStructuredData()
         sint_struct = dict_struct.GetValueForKey("key_sint")
         if not sint_struct.IsValid():
             self.fail("A valid object should have been returned")
 
         # Check Type API
-        if not sint_struct.GetType() == lldb.eStructuredDataTypeSignedInteger:
+        if sint_struct.GetType() != lldb.eStructuredDataTypeSignedInteger:
             self.fail("Wrong type returned: " + str(sint_struct.GetType()))
 
         # Check API returning signed integer value
         output = sint_struct.GetSignedIntegerValue()
-        if not output == -42:
+        if output != -42:
             self.fail("wrong output: " + str(output))
 
         # Calling wrong API on a SBStructuredData
@@ -283,28 +283,26 @@
             self.fail("Valid string " + output + " returned for an integer object")
 
     def double_struct_test(self, dict_struct):
-        floating_point_struct = lldb.SBStructuredData()
         floating_point_struct = dict_struct.GetValueForKey("key_float")
         if not floating_point_struct.IsValid():
             self.fail("A valid object should have been returned")
 
         # Check Type API
-        if not floating_point_struct.GetType() == lldb.eStructuredDataTypeFloat:
+        if floating_point_struct.GetType() != lldb.eStructuredDataTypeFloat:
             self.fail("Wrong type returned: " + str(floating_point_struct.GetType()))
 
         # Check API returning 'double' value
         output = floating_point_struct.GetFloatValue()
-        if not output == 2.99:
+        if output != 2.99:
             self.fail("wrong output: " + str(output))
 
     def bool_struct_test(self, dict_struct):
-        bool_struct = lldb.SBStructuredData()
         bool_struct = dict_struct.GetValueForKey("key_bool")
         if not bool_struct.IsValid():
             self.fail("A valid object should have been returned")
 
         # Check Type API
-        if not bool_struct.GetType() == lldb.eStructuredDataTypeBoolean:
+        if bool_struct.GetType() != lldb.eStructuredDataTypeBoolean:
             self.fail("Wrong type returned: " + str(bool_struct.GetType()))
 
         # Check API returning 'bool' value
@@ -314,17 +312,16 @@
 
     def array_struct_test(self, dict_struct):
         # Check API returning a valid SBStructuredData of 'array' type
-        array_struct = lldb.SBStructuredData()
         array_struct = dict_struct.GetValueForKey("key_array")
         if not array_struct.IsValid():
             self.fail("A valid object should have been returned")
 
         # Check Type API
-        if not array_struct.GetType() == lldb.eStructuredDataTypeArray:
+        if array_struct.GetType() != lldb.eStructuredDataTypeArray:
             self.fail("Wrong type returned: " + str(array_struct.GetType()))
 
         # Check Size API for 'array' type
-        if not array_struct.GetSize() == 2:
+        if array_struct.GetSize() != 2:
             self.fail("Wrong no of elements returned: " + str(array_struct.GetSize()))
 
         # Check API returning a valid SBStructuredData for different 'array'
@@ -332,17 +329,73 @@
         string_struct = array_struct.GetItemAtIndex(0)
         if not string_struct.IsValid():
             self.fail("A valid object should have been returned")
-        if not string_struct.GetType() == lldb.eStructuredDataTypeString:
+        if string_struct.GetType() != lldb.eStructuredDataTypeString:
             self.fail("Wrong type returned: " + str(string_struct.GetType()))
         output = string_struct.GetStringValue(5)
-        if not output == "23":
+        if output != "23":
             self.fail("wrong output: " + str(output))
 
         string_struct = array_struct.GetItemAtIndex(1)
         if not string_struct.IsValid():
             self.fail("A valid object should have been returned")
-        if not string_struct.GetType() == lldb.eStructuredDataTypeString:
+        if string_struct.GetType() != lldb.eStructuredDataTypeString:
             self.fail("Wrong type returned: " + str(string_struct.GetType()))
         output = string_struct.GetStringValue(5)
-        if not output == "arr":
+        if output != "arr":
             self.fail("wrong output: " + str(output))
+
+    def test_round_trip_scalars(self):
+        for original in (0, 11, -1, 0.0, 4.5, -0.25):
+            constructor = type(original)
+            data = lldb.SBStructuredData()
+            data.SetFromJSON(json.dumps(original))
+            round_tripped = constructor(data)
+            self.assertEqual(round_tripped, original)
+
+    def test_dynamic(self):
+        for original in (0, 11, -1, 0.0, 4.5, -0.25, "", "dirk", True, False):
+            data = lldb.SBStructuredData()
+            data.SetFromJSON(json.dumps(original))
+            self.assertEqual(data.dynamic, original)
+
+    def test_round_trip_int(self):
+        for original in (0, 11, -1):
+            data = lldb.SBStructuredData()
+            data.SetFromJSON(json.dumps(original))
+            self.assertEqual(int(data), int(original))
+
+    def test_round_trip_float(self):
+        for original in (0, 11, -1, 0.0, 4.5, -0.25):
+            data = lldb.SBStructuredData()
+            data.SetFromJSON(json.dumps(original))
+            self.assertEqual(float(data), float(original))
+
+    def test_iterate_array(self):
+        array = [0, 1, 2]
+        data = lldb.SBStructuredData()
+        data.SetFromJSON(json.dumps(array))
+        for value in data:
+            self.assertEqual(value, array.pop(0))
+
+    def test_iterate_dictionary(self):
+        dictionary = {"0": 0, "1": 1, "2": 2}
+        keys = set(dictionary.keys())
+        data = lldb.SBStructuredData()
+        data.SetFromJSON(json.dumps(dictionary))
+        for key in data:
+            self.assertIn(key, keys)
+            keys.remove(key)
+
+    def test_getitem_array(self):
+        array = [1, 2, 3]
+        data = lldb.SBStructuredData()
+        data.SetFromJSON(json.dumps(array))
+        for i in range(len(array)):
+            self.assertEqual(data[i], array[i])
+
+    def test_getitem_dictionary(self):
+        dictionary = {"one": 1, "two": 2, "three": 3}
+        data = lldb.SBStructuredData()
+        data.SetFromJSON(json.dumps(dictionary))
+        for key in dictionary:
+            self.assertEqual(data[key], dictionary[key])