Repository: amaranth-lang/amaranth
Pull request #1077 (2024-02-03 02:14:49)

Implement RFC 46: Change `Shape.cast(range(1))` to `unsigned(0)`.
See amaranth-lang/rfcs#46.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1077?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`115954b`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/115954b4d957b4ba642ad056ab1670bf5d185fb6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 86.64% compared to head [(`28205c1`)](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1077?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 86.70%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1077      +/-   ##
==========================================
+ Coverage   86.64%   86.70%   +0.05%     
==========================================
  Files          41       41              
  Lines        7138     7140       +2     
  Branches     1708     1709       +1     
==========================================
+ Hits         6185     6191       +6     
+ Misses        786      785       -1     
+ Partials      167      164       -3     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1077?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).

whitequark: Do we need any documentation changes here?
wanda-phi: > Do we need any documentation changes here?

Good question. I think the wording in lang reference could be somewhat stronger and say that it's not just "wide enough to represent any element", but is explicitly the minimal possible?

But mostly my feeling is that this was an implementation-documentation mismatch fix in the first place.
whitequark: > I think the wording in lang reference could be somewhat stronger and say that it's not just "wide enough to represent any element", but is explicitly the minimal possible?

I think that would be a good change to include here.
wanda-phi: Done.

Patch:
diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py
index 78c16d4..c7eba34 100644
--- a/amaranth/hdl/_ast.py
+++ b/amaranth/hdl/_ast.py
@@ -93,8 +93,8 @@ class Shape:
 
         * a :class:`Shape`, where the result is itself;
         * an :class:`int`, where the result is :func:`unsigned(obj) <unsigned>`;
-        * a :class:`range`, where the result is wide enough to represent any element of the range,
-          and is signed if any element of the range is signed;
+        * a :class:`range`, where the result has minimal width required to represent all elements
+          of the range, and is signed if any element of the range is signed;
         * an :class:`enum.Enum` whose members are all :ref:`constant-castable <lang-constcasting>`
           or :class:`enum.IntEnum`, where the result is wide enough to represent any member of
           the enumeration, and is signed if any member of the enumeration is signed;
@@ -121,6 +121,8 @@ class Shape:
                 signed = obj[0] < 0 or obj[-1] < 0
                 width  = max(bits_for(obj[0], signed),
                              bits_for(obj[-1], signed))
+                if obj[0] == obj[-1] == 0:
+                    width = 0
                 return Shape(width, signed)
             elif isinstance(obj, type) and issubclass(obj, Enum):
                 # For compatibility with third party enumerations, handle them as if they were
diff --git a/docs/changes.rst b/docs/changes.rst
index 2d68cdc..bc25118 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -36,9 +36,11 @@ Implemented RFCs
 
 .. _RFC 17: https://amaranth-lang.org/rfcs/0017-remove-log2-int.html
 .. _RFC 39: https://amaranth-lang.org/rfcs/0039-empty-case.html
+.. _RFC 46: https://amaranth-lang.org/rfcs/0046-shape-range-1.html
 
 * `RFC 17`_: Remove ``log2_int``
 * `RFC 39`_: Change semantics of no-argument ``m.Case()``
+* `RFC 46`_: Change ``Shape.cast(range(1))`` to ``unsigned(0)``
 
 
 Language changes
@@ -52,6 +54,7 @@ Language changes
 * Changed: ``Value.matches()`` with no patterns is ``Const(0)`` instead of ``Const(1)``. (`RFC 39`_)
 * Changed: ``Signal(range(stop), reset=stop)`` warning has been changed into a hard error and made to trigger on any out-of range value.
 * Changed: ``Signal(range(0))`` is now valid without a warning.
+* Changed: ``Shape.cast(range(1))`` is now ``unsigned(0)``. (`RFC 46`_)
 * Deprecated: :func:`amaranth.utils.log2_int`. (`RFC 17`_)
 * Removed: (deprecated in 0.4) :meth:`Const.normalize`. (`RFC 5`_)
 * Removed: (deprecated in 0.4) :class:`Repl`. (`RFC 10`_)


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index cb3e6b9..5c79788 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -115,7 +115,7 @@ class ShapeTestCase(FHDLTestCase):
         self.assertEqual(s3.width, 4)
         self.assertEqual(s3.signed, True)
         s4 = Shape.cast(range(0, 1))
-        self.assertEqual(s4.width, 1)
+        self.assertEqual(s4.width, 0)
         self.assertEqual(s4.signed, False)
         s5 = Shape.cast(range(-1, 0))
         self.assertEqual(s5.width, 1)
@@ -129,6 +129,9 @@ class ShapeTestCase(FHDLTestCase):
         s8 = Shape.cast(range(0, 10, 3))
         self.assertEqual(s8.width, 4)
         self.assertEqual(s8.signed, False)
+        s9 = Shape.cast(range(0, 3, 3))
+        self.assertEqual(s9.width, 0)
+        self.assertEqual(s9.signed, False)
 
     def test_cast_enum(self):
         s1 = Shape.cast(UnsignedEnum)
@@ -1088,7 +1091,7 @@ class SignalTestCase(FHDLTestCase):
         s10 = Signal(range(0))
         self.assertEqual(s10.shape(), unsigned(0))
         s11 = Signal(range(1))
-        self.assertEqual(s11.shape(), unsigned(1))
+        self.assertEqual(s11.shape(), unsigned(0))
 
     def test_shape_wrong(self):
         with self.assertRaisesRegex(TypeError,