Repository: amaranth-lang/amaranth
Pull request #1118 (2024-02-13 04:47:43)

Minor documentation fixes

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1118?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 [(`0056e98`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/0056e982c5a85b26935b8c750cc00cf4b1e66ebc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 85.60% compared to head [(`fc74b58`)](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1118?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) 85.22%.


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


```diff
@@            Coverage Diff             @@
##             main    #1118      +/-   ##
==========================================
- Coverage   85.60%   85.22%   -0.38%     
==========================================
  Files          42       42              
  Lines        8336     8325      -11     
  Branches     1967     1732     -235     
==========================================
- Hits         7136     7095      -41     
- Misses       1004     1033      +29     
- Partials      196      197       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1118?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).


Patch:
diff --git a/amaranth/back/verilog.py b/amaranth/back/verilog.py
index d5c75a1..ef32555 100644
--- a/amaranth/back/verilog.py
+++ b/amaranth/back/verilog.py
@@ -9,9 +9,7 @@ __all__ = ["YosysError", "convert", "convert_fragment"]
 
 def _convert_rtlil_text(rtlil_text, *, strip_internal_attrs=False, write_verilog_opts=()):
     # this version requirement needs to be synchronized with the one in pyproject.toml!
-    # Yosys 0.37 has a critical miscompilation in Verilog backend:
-    # https://github.com/amaranth-lang/amaranth/issues/1049
-    yosys = find_yosys(lambda ver: ver >= (0, 35) and not (0, 36, 79) <= ver <= (0, 37, 29))
+    yosys = find_yosys(lambda ver: ver >= (0, 38))
 
     script = []
     script.append(f"read_ilang <<rtlil\n{rtlil_text}\nrtlil")
diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py
index 6355654..d1d6613 100644
--- a/amaranth/hdl/_ast.py
+++ b/amaranth/hdl/_ast.py
@@ -459,13 +459,11 @@ class Value(metaclass=ABCMeta):
         Returns
         -------
         :ref:`shape-like object <lang-shapelike>`
-
-        ..
-            TODO: while this is documented as returning a shape-like object, in practice we
-            guarantee that this is a concrete Shape. it's unclear whether we will ever want to
-            return a shape-catable object here, but there is not much harm in stating a relaxed
-            contract, as it can always be tightened later, but not vice-versa
         """
+        # TODO: while this is documented as returning a shape-like object, in practice we
+        # guarantee that this is a concrete Shape. it's unclear whether we will ever want to
+        # return a shape-catable object here, but there is not much harm in stating a relaxed
+        # contract, as it can always be tightened later, but not vice-versa
         pass # :nocov:
 
     def as_unsigned(self):
@@ -483,7 +481,14 @@ class Value(metaclass=ABCMeta):
         Returns
         -------
         :class:`Value`, :pc:`signed(len(self))`, :ref:`assignable <lang-assignable>`
+
+        Raises
+        ------
+        ValueError
+            If :pc:`len(self) == 0`.
         """
+        if len(self) == 0:
+            raise ValueError("Cannot create a 0-width signed value")
         return Operator("s", [self])
 
     def __bool__(self):
@@ -846,7 +851,7 @@ class Value(metaclass=ABCMeta):
         return Operator("^", [other, self])
 
     def xor(self):
-        """Reduction XOR; is an odd amount of bits :pc:`1`?
+        """Reduction XOR; are an odd amount of bits :pc:`1`?
 
         Returns
         -------
@@ -900,9 +905,9 @@ class Value(metaclass=ABCMeta):
 
         Returns
         -------
-        :class:`Value`, :pc:`unsigned(len(self) + amount)`
+        :class:`Value`, :pc:`unsigned(max(len(self) + amount, 0))`
             If :pc:`self` is unsigned.
-        :class:`Value`, :pc:`signed(len(self) + amount)`
+        :class:`Value`, :pc:`signed(max(len(self) + amount, 1))`
             If :pc:`self` is signed.
         """
         if not isinstance(amount, int):
@@ -974,6 +979,8 @@ class Value(metaclass=ABCMeta):
         if amount < 0:
             return self.shift_left(-amount)
         if self.shape().signed:
+            if amount >= len(self):
+                amount = len(self) - 1
             return self[amount:].as_signed()
         else:
             return self[amount:] # unsigned
@@ -1049,8 +1056,12 @@ class Value(metaclass=ABCMeta):
         """Part-select with bit granularity.
 
         Selects a constant width, variable offset part of :pc:`self`, where parts with successive
-        offsets overlap by :pc:`width - 1` bits. When :pc:`offset` is a constant integer, this
-        operation is equivalent to :pc:`self[offset:offset + width]`.
+        offsets overlap by :pc:`width - 1` bits. Bits above the most significant bit of :pc:`self`
+        may be selected; they are equal to zero if :pc:`self` is unsigned, to :pc:`self[-1]` if
+        :pc:`self` is signed, and assigning to them does nothing.
+
+        When :pc:`offset` is a constant integer and :pc:`offset + width <= len(self)`,
+        this operation is equivalent to :pc:`self[offset:offset + width]`.
 
         Parameters
         ----------
@@ -1079,8 +1090,12 @@ class Value(metaclass=ABCMeta):
         """Part-select with word granularity.
 
         Selects a constant width, variable offset part of :pc:`self`, where parts with successive
-        offsets are adjacent but do not overlap. When :pc:`offset` is a constant integer, this
-        operation is equivalent to :pc:`self[offset * width:(offset + 1) * width]`.
+        offsets are adjacent but do not overlap. Bits above the most significant bit of :pc:`self`
+        may be selected; they are equal to zero if :pc:`self` is unsigned, to :pc:`self[-1]` if
+        :pc:`self` is signed, and assigning to them does nothing.
+
+        When :pc:`offset` is a constant integer and :pc:`width:(offset + 1) * width <= len(self)`,
+        this operation is equivalent to :pc:`self[offset * width:(offset + 1) * width]`.
 
         Parameters
         ----------
@@ -1253,9 +1268,6 @@ class ValueCastable:
         if cls.shape is ValueCastable.shape:
             raise TypeError(f"Class '{cls.__name__}' deriving from 'ValueCastable' must override "
                             "the 'shape' method")
-        if not hasattr(cls.as_value, "_ValueCastable__memoized"):
-            raise TypeError(f"Class '{cls.__name__}' deriving from 'ValueCastable' must decorate "
-                            "the 'as_value' method with the 'ValueCastable.lowermethod' decorator")
 
     # The signatures and definitions of these methods are weird because they are present here for
     # documentation (and error checking above) purpose only and should not affect control flow.
@@ -1324,17 +1336,10 @@ class ValueCastable:
         """
         return super().shape(*args, **kwargs) # :nocov:
 
+    # TODO(amaranth-0.6): remove
     @staticmethod
+    @deprecated("`ValueCastable.lowermethod` is no longer required and will be removed in Amaranth 0.6")
     def lowermethod(func):
-        """Decorator to memoize lowering methods.
-
-        Ensures the decorated method is called only once, with subsequent method calls returning
-        the object returned by the first first method call.
-
-        This decorator is required to decorate the :pc:`as_value` method of :pc:`ValueCastable`
-        subclasses. This is to ensure that Amaranth's view of representation of all values stays
-        internally consistent.
-        """
         @functools.wraps(func)
         def wrapper_memoized(self, *args, **kwargs):
             # Use `in self.__dict__` instead of `hasattr` to avoid interfering with custom
diff --git a/amaranth/hdl/_rec.py b/amaranth/hdl/_rec.py
index a02b69b..f033fad 100644
--- a/amaranth/hdl/_rec.py
+++ b/amaranth/hdl/_rec.py
@@ -178,7 +178,6 @@ class Record(ValueCastable):
                 raise AttributeError("{} does not have a field '{}'. Did you mean one of: {}?"
                                      .format(reference, item, ", ".join(self.fields))) from None
 
-    @ValueCastable.lowermethod
     def as_value(self):
         return Cat(self.fields.values())
 
diff --git a/amaranth/lib/data.py b/amaranth/lib/data.py
index 1083fa3..978f83e 100644
--- a/amaranth/lib/data.py
+++ b/amaranth/lib/data.py
@@ -638,7 +638,6 @@ class View(ValueCastable):
         """
         return self.__orig_layout
 
-    @ValueCastable.lowermethod
     def as_value(self):
         """Get underlying value.
 
diff --git a/amaranth/lib/enum.py b/amaranth/lib/enum.py
index 998d54e..84b922d 100644
--- a/amaranth/lib/enum.py
+++ b/amaranth/lib/enum.py
@@ -232,7 +232,6 @@ class EnumView(ValueCastable):
         """Returns the underlying enum type."""
         return self.enum
 
-    @ValueCastable.lowermethod
     def as_value(self):
         """Returns the underlying value."""
         return self.target
diff --git a/amaranth/vendor/_lattice_ice40.py b/amaranth/vendor/_lattice_ice40.py
index 6f0447a..0894025 100644
--- a/amaranth/vendor/_lattice_ice40.py
+++ b/amaranth/vendor/_lattice_ice40.py
@@ -426,12 +426,11 @@ class LatticeICE40Platform(TemplatedPlatform):
     def _get_io_buffer(self, m, pin, port, attrs, *, i_invert=False, o_invert=False,
                        invert_lut=False):
         def get_dff(clk, d, q):
-            m.submodules += Instance("$dff",
-                p_CLK_POLARITY=1,
-                p_WIDTH=len(d),
-                i_CLK=clk,
-                i_D=d,
-                o_Q=q)
+            for bit in range(len(d)):
+                m.submodules += Instance("SB_DFF",
+                    i_C=clk,
+                    i_D=d[bit],
+                    o_Q=q[bit])
 
         def get_ineg(y, invert):
             if invert_lut:
diff --git a/pyproject.toml b/pyproject.toml
index b866af0..96f976e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,7 +20,7 @@ dependencies = [
 
 [project.optional-dependencies]
 # this version requirement needs to be synchronized with the one in amaranth.back.verilog!
-builtin-yosys = ["amaranth-yosys>=0.35,!=0.37.0.*"]
+builtin-yosys = ["amaranth-yosys>=0.38"]
 remote-build  = ["paramiko~=2.7"]
 
 [project.scripts]


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 83bbb77..4b14f6b 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -343,7 +343,7 @@ class ValueTestCase(FHDLTestCase):
         self.assertRepr(Const(256, signed(9)).shift_left(-5),
                         "(s (slice (const 9'sd-256) 5:9))")
         self.assertRepr(Const(256, signed(9)).shift_left(-15),
-                        "(s (slice (const 9'sd-256) 9:9))")
+                        "(s (slice (const 9'sd-256) 8:9))")
 
     def test_shift_left_wrong(self):
         with self.assertRaisesRegex(TypeError,
@@ -367,12 +367,20 @@ class ValueTestCase(FHDLTestCase):
                         "(slice (const 9'd256) 1:9)")
         self.assertRepr(Const(256, unsigned(9)).shift_right(5),
                         "(slice (const 9'd256) 5:9)")
+        self.assertRepr(Const(256, unsigned(9)).shift_right(15),
+                        "(slice (const 9'd256) 9:9)")
         self.assertRepr(Const(256, signed(9)).shift_right(1),
                         "(s (slice (const 9'sd-256) 1:9))")
         self.assertRepr(Const(256, signed(9)).shift_right(5),
                         "(s (slice (const 9'sd-256) 5:9))")
+        self.assertRepr(Const(256, signed(9)).shift_right(7),
+                        "(s (slice (const 9'sd-256) 7:9))")
+        self.assertRepr(Const(256, signed(9)).shift_right(8),
+                        "(s (slice (const 9'sd-256) 8:9))")
+        self.assertRepr(Const(256, signed(9)).shift_right(9),
+                        "(s (slice (const 9'sd-256) 8:9))")
         self.assertRepr(Const(256, signed(9)).shift_right(15),
-                        "(s (slice (const 9'sd-256) 9:9))")
+                        "(s (slice (const 9'sd-256) 8:9))")
 
     def test_shift_right_wrong(self):
         with self.assertRaisesRegex(TypeError,
@@ -516,6 +524,11 @@ class OperatorTestCase(FHDLTestCase):
         self.assertEqual(repr(v), "(s (const 4'd1))")
         self.assertEqual(v.shape(), signed(4))
 
+    def test_as_signed_wrong(self):
+        with self.assertRaisesRegex(ValueError,
+                r"^Cannot create a 0-width signed value$"):
+            Const(0, 0).as_signed()
+
     def test_pos(self):
         self.assertRepr(+Const(10), "(const 4'd10)")
 
@@ -1027,7 +1040,6 @@ class ArrayTestCase(FHDLTestCase):
 
     def test_index_value_castable(self):
         class MyValue(ValueCastable):
-            @ValueCastable.lowermethod
             def as_value(self):
                 return Signal()
 
@@ -1315,7 +1327,6 @@ class MockValueCastable(ValueCastable):
     def shape(self):
         return Value.cast(self.dest).shape()
 
-    @ValueCastable.lowermethod
     def as_value(self):
         return self.dest
 
@@ -1327,9 +1338,10 @@ class MockValueCastableChanges(ValueCastable):
     def shape(self):
         return unsigned(self.width)
 
-    @ValueCastable.lowermethod
-    def as_value(self):
-        return Signal(self.width)
+    with _ignore_deprecated():
+        @ValueCastable.lowermethod
+        def as_value(self):
+            return Signal(self.width)
 
 
 class MockValueCastableCustomGetattr(ValueCastable):
@@ -1339,29 +1351,16 @@ class MockValueCastableCustomGetattr(ValueCastable):
     def shape(self):
         assert False
 
-    @ValueCastable.lowermethod
-    def as_value(self):
-        return Const(0)
+    with _ignore_deprecated():
+        @ValueCastable.lowermethod
+        def as_value(self):
+            return Const(0)
 
     def __getattr__(self, attr):
         assert False
 
 
 class ValueCastableTestCase(FHDLTestCase):
-    def test_not_decorated(self):
-        with self.assertRaisesRegex(TypeError,
-                r"^Class 'MockValueCastableNotDecorated' deriving from 'ValueCastable' must "
-                r"decorate the 'as_value' method with the 'ValueCastable.lowermethod' decorator$"):
-            class MockValueCastableNotDecorated(ValueCastable):
-                def __init__(self):
-                    pass
-
-                def shape(self):
-                    pass
-
-                def as_value(self):
-                    return Signal()
-
     def test_no_override(self):
         with self.assertRaisesRegex(TypeError,
                 r"^Class 'MockValueCastableNoOverrideAsValue' deriving from 'ValueCastable' must "
diff --git a/tests/test_lib_wiring.py b/tests/test_lib_wiring.py
index 98f48c8..1682f78 100644
--- a/tests/test_lib_wiring.py
+++ b/tests/test_lib_wiring.py
@@ -4,7 +4,6 @@ import unittest
 from types import SimpleNamespace as NS
 
 from amaranth import *
-from amaranth.hdl._ast import ValueCastable
 from amaranth.lib import data, enum
 from amaranth.lib.wiring import Flow, In, Out, Member
 from amaranth.lib.wiring import SignatureError, SignatureMembers, FlippedSignatureMembers
diff --git a/tests/test_sim.py b/tests/test_sim.py
index fd90e8d..01e55d3 100644
--- a/tests/test_sim.py
+++ b/tests/test_sim.py
@@ -728,7 +728,6 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
 
     def test_value_castable(self):
         class MyValue(ValueCastable):
-            @ValueCastable.lowermethod
             def as_value(self):
                 return Signal()