Repository: amaranth-lang/amaranth
Pull request #1478 (2024-08-10 09:51:20)

hdl._ast: name Formatter instance uniquely.
I noticed an `Enum` could only be used as the last (or sole) component in a format string, otherwise it'd raise:

```
/nix/store/bfp9vca8qrshj749s346ddjc41kj6zxx-python3.12-amaranth-0.5.1/lib/python3.12/site-packages/amaranth/hdl/_ast.py:2542: in __init__
    obj = get_field(field_name)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

field_name = '2'

    def get_field(field_name):
        nonlocal auto_arg_index
        if field_name == "":
            if auto_arg_index is None:
                raise ValueError("cannot switch from manual field "
                                    "specification to automatic field "
                                    "numbering")
            field_name = str(auto_arg_index)
            auto_arg_index += 1
        elif field_name.isdigit():
            if auto_arg_index is not None and auto_arg_index > 0:
                raise ValueError("cannot switch from automatic field "
                                    "numbering to manual field "
                                    "specification")
            auto_arg_index = None

>       obj, arg_used = fmt.get_field(field_name, args, kwargs)
E       AttributeError: 'Enum' object has no attribute 'get_field'

/nix/store/bfp9vca8qrshj749s346ddjc41kj6zxx-python3.12-amaranth-0.5.1/lib/python3.12/site-packages/amaranth/hdl/_ast.py:2524: AttributeError
```

 Commit message of b02139b848e88d3c64a8056f83ee57cfbf481dca follows:

---

We use the name `fmt` below, accidentally reassigning this. A ShapeCastable (such as an Enum) used in formatting would cause a crash if any subsequent value was formatted.

It seems plausible the name `fmt` will get used again for other `shape.format()` returns, so use `fmtter` for the Formatter instead.

whitequark: After this gets merged, could you please open a backport PR as well? The same thing against the v0.5.x branch.

Patch:
diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py
index 096f528..d99cd72 100644
--- a/amaranth/hdl/_ast.py
+++ b/amaranth/hdl/_ast.py
@@ -2500,7 +2500,7 @@ class _FormatLike:
 @final
 class Format(_FormatLike):
     def __init__(self, format, *args, **kwargs):
-        fmt = string.Formatter()
+        fmtter = string.Formatter()
         chunks = []
         used_args = set()
         auto_arg_index = 0
@@ -2521,29 +2521,29 @@ class Format(_FormatLike):
                                         "specification")
                 auto_arg_index = None
 
-            obj, arg_used = fmt.get_field(field_name, args, kwargs)
+            obj, arg_used = fmtter.get_field(field_name, args, kwargs)
             used_args.add(arg_used)
             return obj
 
         def subformat(sub_string):
             result = []
-            for literal, field_name, format_spec, conversion in fmt.parse(sub_string):
+            for literal, field_name, format_spec, conversion in fmtter.parse(sub_string):
                 result.append(literal)
                 if field_name is not None:
                     obj = get_field(field_name)
-                    obj = fmt.convert_field(obj, conversion)
+                    obj = fmtter.convert_field(obj, conversion)
                     format_spec = subformat(format_spec)
-                    result.append(fmt.format_field(obj, format_spec))
+                    result.append(fmtter.format_field(obj, format_spec))
             return "".join(result)
 
-        for literal, field_name, format_spec, conversion in fmt.parse(format):
+        for literal, field_name, format_spec, conversion in fmtter.parse(format):
             chunks.append(literal)
             if field_name is not None:
                 obj = get_field(field_name)
                 if conversion == "v":
                     obj = Value.cast(obj)
                 else:
-                    obj = fmt.convert_field(obj, conversion)
+                    obj = fmtter.convert_field(obj, conversion)
                 format_spec = subformat(format_spec)
                 if isinstance(obj, Value):
                     # Perform validation.
@@ -2565,7 +2565,7 @@ class Format(_FormatLike):
                         raise ValueError(f"Format specifiers ({format_spec!r}) cannot be used for 'Format' objects")
                     chunks += obj._as_format()._chunks
                 else:
-                    chunks.append(fmt.format_field(obj, format_spec))
+                    chunks.append(fmtter.format_field(obj, format_spec))
 
         for i in range(len(args)):
             if i not in used_args:


Test patch:
diff --git a/tests/test_sim.py b/tests/test_sim.py
index 43e94e4..deb305d 100644
--- a/tests/test_sim.py
+++ b/tests/test_sim.py
@@ -1263,6 +1263,33 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
             [unknown]
         """))
 
+    def test_print_enum_followed_by(self):
+        class MyEnum(enum.Enum, shape=unsigned(2)):
+            A = 0
+            B = 1
+            CDE = 2
+
+        sig = Signal(MyEnum)
+        ctr = Signal(2)
+        m = Module()
+        m.d.comb += sig.eq(ctr)
+        m.d.sync += [
+            Print(Format("{} {}", sig, ctr)),
+            ctr.eq(ctr + 1),
+        ]
+        output = StringIO()
+        with redirect_stdout(output):
+            with self.assertSimulation(m) as sim:
+                sim.add_clock(1e-6, domain="sync")
+                async def testbench(ctx):
+                    await ctx.tick().repeat(4)
+                sim.add_testbench(testbench)
+        self.assertEqual(output.getvalue(), dedent("""\
+            A 0
+            B 1
+            CDE 2
+            [unknown] 3
+        """))
 
     def test_assert(self):
         m = Module()