[Lldb-commits] [lldb] [lldb] Update formatter_bytecode.py to ABI version 2 (PR #229923)

via lldb-commits lldb-commits at lists.llvm.org
Wed Oct 7 14:58:50 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lldb

Author: Dave Lee (kastiglione)

<details>
<summary>Changes</summary>

Update the assembler and the Python to bytecode compiler to target the version
2 formatter ABI, in which `self` is a Dictionary owned by LLDB and passed as
the first argument to every synthetic method.

The primary feature of this change is the separation of storage, locals are on
the stack, attributes are stored in the `self` Dictionary. Since locals and
attrs no longer intermingle in the stack, local variables are now allowed in
`__init__` and `update`, and attributes can be reassigned.

In the compiler:

* `self.x = expr` is compiled to `0 pick "x" <expr> dict_set`
* `self.x` is compiled to `0 pick "x" dict_get`
* Arguments and local variables are on the stack above `self`
* An `__init__` that only stores `valobj` is not emitted, since this is
  what LLDB does when there is no `@<!-- -->init`

Assisted-by: claude


---
Full diff: https://github.com/llvm/llvm-project/pull/229923.diff


3 Files Affected:

- (modified) lldb/examples/python/formatter_bytecode.py (+163-59) 
- (modified) lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterC.txt (+8-8) 
- (modified) lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterSwift.txt (+9-9) 


``````````diff
diff --git a/lldb/examples/python/formatter_bytecode.py b/lldb/examples/python/formatter_bytecode.py
index 188f18a351189d..7b1cae4d5f411c 100644
--- a/lldb/examples/python/formatter_bytecode.py
+++ b/lldb/examples/python/formatter_bytecode.py
@@ -25,7 +25,7 @@
 from dataclasses import dataclass
 from typing import Any, BinaryIO, Optional, Sequence, TextIO, Tuple, Union, cast
 
-BINARY_VERSION = 1
+BINARY_VERSION = 2
 
 # Types
 type_String = 1
@@ -881,19 +881,18 @@ class Compiler(ast.NodeVisitor):
     # Variables
 
     The compiler supports two kinds of variables, local variables and attribute
-    variables (properties), but there are limitations on both.
+    variables (properties).
 
-    In __init__ and update, local variables are currently *not* supported, but
-    attributes can be assigned to. This matches the common case for these
-    functions.
+    Attributes are stored in `self`, which in the version 2 ABI is a Dictionary
+    owned by LLDB and passed as the first argument (data[0]) to every synthetic
+    method. Attribute writes (`self.x = expr`) are lowered to `dict_set`, and
+    attribute reads (`self.x`) are lowered to `dict_get`. Attributes can only
+    be assigned to in __init__ and update, which matches the common case.
 
-    In all other function bodies, local variables _are_ supported, but
-    attributes can only be read from, *not* assigned to. This also matches the
-    common case for these functions.
-
-    Variables (local and attributes) are tracked, allowing the compiler to know
-    their position in the stack. Variable reads can then be lowered to `pick`
-    instructions. See the compiler's `locals` and `attrs` attributes.
+    Local variables (including method arguments) live on the data stack, above
+    `self`. They are tracked, allowing the compiler to know their position in
+    the stack. Local variable reads are then lowered to `pick` instructions.
+    See the compiler's `locals` and `locals_base` attributes.
 
     # Functions
 
@@ -909,11 +908,13 @@ class Compiler(ast.NodeVisitor):
     # oldest/deepest; locals[-1] is the most recently pushed.
     locals: list[str]
 
-    # Names of visible attrs in bottom-to-top stack order. Always holds the
-    # full combined frame for the method being compiled: grows incrementally
-    # during __init__/update, and is set to the combined list before getter
-    # methods are compiled.
-    attrs: list[str]
+    # Stack index of locals[0]. In synthetic methods, data[0] holds `self`, so
+    # locals start at 1. In top-level functions (summaries), locals start at 0.
+    locals_base: int
+
+    # Names of attrs known to be stored in `self`. Grows during __init__ and
+    # update, which are compiled before the getter methods.
+    attrs: set[str]
 
     # Bytecode signature of the method being compiled, or None for top-level
     # functions.
@@ -923,7 +924,8 @@ class Compiler(ast.NodeVisitor):
 
     def __init__(self) -> None:
         self.locals = []
-        self.attrs = []
+        self.locals_base = 0
+        self.attrs = set()
         self.current_sig = None
         self.buffer = io.StringIO()
 
@@ -943,11 +945,16 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:
                     raise CompilerError(f"unsupported method: {item.name}", item)
                 methods[item.name] = item
 
-        self.attrs = []
+        self.attrs = set()
+
         if method := methods.get("__init__"):
             self._compile_method(method)
-        # self.attrs now holds init's attrs. update's attrs are appended above
-        # them, so after update self.attrs is the combined init+update list.
+        else:
+            # Without an @init, LLDB stores the value in self["valobj"].
+            self.attrs.add("valobj")
+
+        # update is compiled next, so that self.attrs holds all assigned attrs
+        # before the getter methods are compiled.
         if method := methods.get("update"):
             self._compile_method(method)
 
@@ -973,6 +980,7 @@ def _compile_method(self, node: ast.FunctionDef) -> None:
             args.pop()  # drop trailing 'internal_dict'
 
         self.locals = [arg.arg for arg in args]
+        self.locals_base = 1
 
         # Compile into a temporary buffer so the signature line can be
         # emitted first.
@@ -992,8 +1000,9 @@ def _compile_method(self, node: ast.FunctionDef) -> None:
     def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
         # Top-level function (not inside a class).
         self.current_sig = None
-        self.attrs = []
+        self.attrs = set()
         self.locals = [arg.arg for arg in node.args.args]
+        self.locals_base = 0
         self._visit_each(node.body)
         self.locals.clear()
 
@@ -1068,33 +1077,16 @@ def visit_Assign(self, node: ast.Assign) -> None:
                     node,
                 )
 
+            # self[attr] = value
             attr = target.attr
-            if attr in self.attrs:
-                raise CompilerError(f"attribute '{attr}' is already assigned", node)
-
-            # If the RHS is an argument (the only kind of local permitted in
-            # __init__) - then it is already on the stack in place, and no
-            # evaluation is needed.
-            is_arg = (
-                isinstance(node.value, ast.Name)
-                and self._local_index(node.value) is not None
-            )
-            if not is_arg:
-                # Evaluate the RHS, leaving its value on the stack.
-                self.visit(node.value)
-
-            # Record the attr.
-            self.attrs.append(attr)
+            self._output("0 pick")
+            self._output(f'"{attr}"')
+            self.visit(node.value)
+            self._output("dict_set")
+            self.attrs.add(attr)
             return
 
         # Handle local variable assignment.
-        if self.current_sig in ("@init", "@update"):
-            raise CompilerError(
-                "local variable assignment is not allowed in __init__ or update; "
-                "use attribute assignment (self.attr = ...) instead",
-                node,
-            )
-
         if isinstance(target, ast.Name):
             names = [target]
         elif isinstance(target, ast.Tuple):
@@ -1120,8 +1112,12 @@ def visit_Attribute(self, node: ast.Attribute) -> None:
             raise CompilerError(
                 "unsupported attribute access (only self.attr is supported)", node
             )
-        pick_idx = self._attr_index(node.attr, node)
-        self._output(f"{pick_idx} pick")  # "# self.{node.attr}"
+        if node.attr not in self.attrs:
+            raise CompilerError(f"unknown attribute: {node.attr}", node)
+        # self[attr]
+        self._output("0 pick")
+        self._output(f'"{node.attr}"')
+        self._output("dict_get")
 
     def visit_Name(self, node: ast.Name) -> None:
         idx = self._local_index(node)
@@ -1133,19 +1129,9 @@ def _visit_each(self, nodes: Sequence[ast.AST]) -> None:
         for child in nodes:
             self.visit(child)
 
-    def _attr_index(self, name: str, node: ast.expr) -> int:
-        # self.attrs is always the full visible attr frame, so the index is
-        # the direct pick offset with no further adjustment.
-        try:
-            return self.attrs.index(name)
-        except ValueError:
-            raise CompilerError(f"unknown attribute: {name}", node)
-
     def _local_index(self, name: ast.Name) -> Optional[int]:
         try:
-            idx = self.locals.index(name.id)
-            # Offset past all attrs.
-            return len(self.attrs) + idx
+            return self.locals_base + self.locals.index(name.id)
         except ValueError:
             return None
 
@@ -1299,6 +1285,7 @@ def _main():
     ############################################################################
     # Tests.
     ############################################################################
+    import tempfile
     import unittest
 
     class TestAssembler(unittest.TestCase):
@@ -1382,7 +1369,7 @@ def test_write_source(self):
 
             self.assertIn("__attribute__((used, section(FORMATTER_SECTION)))", src)
             self.assertIn("unsigned char _Account_formatter[] =", src)
-            self.assertIn('"\\x01"', src)  # version
+            self.assertIn('// version\n    "\\x02"', src)  # version
             self.assertIn('"\\x15"', src)  # record size (21)
             self.assertIn('"\\x07"', src)  # type name size (7)
             self.assertIn('"Account"', src)  # type name
@@ -1417,4 +1404,121 @@ def test_write_source(self):
             disassemble_file(binary, dis)
             self.assertEqual(dis.getvalue(), "@summary: return\n")
 
+    class TestCompiler(unittest.TestCase):
+
+        class FakeValue:
+            """A minimal stand-in for SBValue."""
+
+            def __init__(self, value=0, children=None):
+                self.value = value
+                self.children = children or {}
+
+            def GetChildMemberWithName(self, name):
+                return self.children[name]
+
+            def GetChildAtIndex(self, idx):
+                return list(self.children.values())[idx]
+
+            def GetSyntheticValue(self):
+                return self
+
+            def GetValueAsUnsigned(self):
+                return self.value
+
+        def compile_methods(self, source):
+            with tempfile.NamedTemporaryFile("w", suffix=".py") as f:
+                f.write(textwrap.dedent(source))
+                f.flush()
+                assembly = Compiler().compile(f.name)
+            section = assemble_file("T", io.StringIO(assembly))
+            return dict(section.signatures)
+
+        def test_synthetic(self):
+            methods = self.compile_methods("""
+                import lldb
+
+                class Synthetic:
+                    def __init__(self, valobj, _):
+                        self.valobj = valobj
+
+                    def update(self) -> bool:
+                        self.storage = self.valobj.GetChildMemberWithName("_storage")
+                        self.count = self.valobj.GetChildMemberWithName(
+                            "_count"
+                        ).GetValueAsUnsigned()
+                        return True
+
+                    def num_children(self):
+                        return self.count
+
+                    def get_child_at_index(self, idx):
+                        if idx < self.count:
+                            return self.storage.GetChildAtIndex(idx)
+                        return None
+                """)
+
+            FakeValue = self.FakeValue
+            a, b = FakeValue(10), FakeValue(20)
+            valobj = FakeValue(
+                children={
+                    "_storage": FakeValue(children={"a": a, "b": b}),
+                    "_count": FakeValue(2),
+                }
+            )
+
+            # Without @init, LLDB stores the value in self["valobj"].
+            self_dict = {"valobj": valobj}
+            self.assertEqual(interpret(methods["update"], [], [self_dict]), 1)
+            self.assertEqual(self_dict["count"], 2)
+            self.assertEqual(interpret(methods["get_num_children"], [], [self_dict]), 2)
+            self.assertIs(
+                interpret(methods["get_child_at_index"], [], [self_dict, 1]), b
+            )
+            self.assertIsNone(
+                interpret(methods["get_child_at_index"], [], [self_dict, 2])
+            )
+
+        def test_init(self):
+            methods = self.compile_methods(
+                """
+                class Synthetic:
+                    def __init__(self, valobj, _):
+                        self.storage = valobj.GetChildMemberWithName("_storage")
+
+                    def update(self) -> bool:
+                        size = self.storage.GetValueAsUnsigned()
+                        self.size = size
+                        return False
+
+                    def num_children(self):
+                        return self.size
+                """
+            )
+            storage = self.FakeValue(3)
+            valobj = self.FakeValue(children={"_storage": storage})
+            self_dict = {}
+            interpret(methods["init"], [], [self_dict, valobj])
+            self.assertIs(self_dict["storage"], storage)
+            self.assertEqual(interpret(methods["update"], [], [self_dict]), 0)
+            self.assertEqual(interpret(methods["get_num_children"], [], [self_dict]), 3)
+
+        def test_errors(self):
+            with self.assertRaisesRegex(CompilerError, "unknown attribute: missing"):
+                self.compile_methods(
+                    """
+                    class Synthetic:
+                        def num_children(self):
+                            return self.missing
+                    """
+                )
+            with self.assertRaisesRegex(CompilerError, "only allowed in __init__"):
+                self.compile_methods(
+                    """
+                    class Synthetic:
+                        def num_children(self):
+                            self.count = 1
+                            return 1
+                    """
+                )
+
     unittest.main(argv=[__file__])
diff --git a/lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterC.txt b/lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterC.txt
index ea6f7c924bd445..09e60e2f1d05ad 100644
--- a/lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterC.txt
+++ b/lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterC.txt
@@ -8,9 +8,9 @@
 __attribute__((used, section(FORMATTER_SECTION)))
 unsigned char _RigidArray_formatter[] =
     // version
-    "\x01"
+    "\x02"
     // remaining record size
-    "\x4a"
+    "\x86\x01"
     // type name size
     "\x0a"
     // type name
@@ -20,19 +20,19 @@ unsigned char _RigidArray_formatter[] =
     // sig_update
     "\x06"
     // program size
-    "\x2a"
+    "\x54"
     // program
-    "\x24\x00\x03\x22\x08\x5f\x73\x74\x6f\x72\x61\x67\x65\x23\x12\x60\x23\x18\x60\x24\x00\x03\x22\x06\x5f\x63\x6f\x75\x6e\x74\x23\x12\x60\x23\x18\x60\x23\x21\x60\x24\x01\x13"
+    "\x24\x00\x03\x22\x07\x73\x74\x6f\x72\x61\x67\x65\x24\x00\x03\x22\x06\x76\x61\x6c\x6f\x62\x6a\x72\x22\x08\x5f\x73\x74\x6f\x72\x61\x67\x65\x23\x12\x60\x23\x18\x60\x71\x24\x00\x03\x22\x05\x63\x6f\x75\x6e\x74\x24\x00\x03\x22\x06\x76\x61\x6c\x6f\x62\x6a\x72\x22\x06\x5f\x63\x6f\x75\x6e\x74\x23\x12\x60\x23\x18\x60\x23\x21\x60\x71\x24\x01\x13"
     // sig_get_num_children
     "\x02"
     // program size
-    "\x04"
+    "\x0c"
     // program
-    "\x24\x02\x03\x13"
+    "\x24\x00\x03\x22\x05\x63\x6f\x75\x6e\x74\x72\x13"
     // sig_get_child_at_index
     "\x04"
     // program size
-    "\x0a"
+    "\x14"
     // program
-    "\x24\x01\x03\x24\x03\x03\x23\x11\x60\x13"
+    "\x24\x00\x03\x22\x07\x73\x74\x6f\x72\x61\x67\x65\x72\x24\x01\x03\x23\x11\x60\x13"
 ;
diff --git a/lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterSwift.txt b/lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterSwift.txt
index e13aaa75aee0b6..646ac1518c1354 100644
--- a/lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterSwift.txt
+++ b/lldb/test/Shell/ScriptInterpreter/Python/Inputs/FormatterBytecode/RigidArrayLLDBFormatterSwift.txt
@@ -5,11 +5,11 @@
 @section(".lldbformatters")
 #endif
 @used
-let `RigidArray formatter`: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (
+let `RigidArray formatter`: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (
     // version
-    0x01,
+    0x02,
     // remaining record size
-    0x4a,
+    0x86, 0x01,
     // type name size
     0x0a,
     // type name
@@ -19,20 +19,20 @@ let `RigidArray formatter`: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UI
     // sig_update
     0x06,
     // program size
-    0x2a,
+    0x54,
     // program
-    0x24, 0x00, 0x03, 0x22, 0x08, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x23, 0x12, 0x60, 0x23, 0x18, 0x60, 0x24, 0x00, 0x03, 0x22, 0x06, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x23, 0x12, 0x60, 0x23, 0x18, 0x60, 0x23, 0x21, 0x60, 0x24, 0x01, 0x13,
+    0x24, 0x00, 0x03, 0x22, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x24, 0x00, 0x03, 0x22, 0x06, 0x76, 0x61, 0x6c, 0x6f, 0x62, 0x6a, 0x72, 0x22, 0x08, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x23, 0x12, 0x60, 0x23, 0x18, 0x60, 0x71, 0x24, 0x00, 0x03, 0x22, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x24, 0x00, 0x03, 0x22, 0x06, 0x76, 0x61, 0x6c, 0x6f, 0x62, 0x6a, 0x72, 0x22, 0x06, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x23, 0x12, 0x60, 0x23, 0x18, 0x60, 0x23, 0x21, 0x60, 0x71, 0x24, 0x01, 0x13,
     // sig_get_num_children
     0x02,
     // program size
-    0x04,
+    0x0c,
     // program
-    0x24, 0x02, 0x03, 0x13,
+    0x24, 0x00, 0x03, 0x22, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x13,
     // sig_get_child_at_index
     0x04,
     // program size
-    0x0a,
+    0x14,
     // program
-    0x24, 0x01, 0x03, 0x24, 0x03, 0x03, 0x23, 0x11, 0x60, 0x13,
+    0x24, 0x00, 0x03, 0x22, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x72, 0x24, 0x01, 0x03, 0x23, 0x11, 0x60, 0x13,
 )
 #endif

``````````

</details>


https://github.com/llvm/llvm-project/pull/229923


More information about the lldb-commits mailing list