Repository: amaranth-lang/amaranth
Pull request #908 (2023-09-13 14:58:54)

Elaboratable does not need ABCMeta as its metaclass
This was introduced in commit 44711b7d, and was never used within Amaranth itself. While technically a breaking change I think this will not cause enough breakage to warrant a deprecation cycle (nor can we make this a deprecation this without a lot of work).

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/908?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
> Merging [#908](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/908?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) (55c6281) into [main](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/d32ca043263c04bdd78b64fa912185a61169a683?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) (d32ca04) will **decrease** coverage by `0.03%`.
> Report is 1 commits behind head on main.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main     #908      +/-   ##
==========================================
- Coverage   83.75%   83.73%   -0.03%     
==========================================
  Files          54       54              
  Lines        7678     7677       -1     
  Branches     1867     1867              
==========================================
- Hits         6431     6428       -3     
- Misses       1048     1050       +2     
  Partials      199      199              
```


| [Files Changed](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/908?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) | Coverage Δ | |
|---|---|---|
| [amaranth/hdl/ir.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/908?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvaGRsL2lyLnB5) | `95.31% <100.00%> (ø)` | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/908/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang)


Patch:
diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py
index ed93347..7655d8a 100644
--- a/amaranth/hdl/ast.py
+++ b/amaranth/hdl/ast.py
@@ -604,7 +604,7 @@ class Value(metaclass=ABCMeta):
 
     @abstractmethod
     def _rhs_signals(self):
-        pass # :nocov:
+        raise NotImplementedError # :nocov:
 
 
 @final
@@ -1056,12 +1056,15 @@ class Signal(Value, DUID, metaclass=_SignalMeta):
                                  .format(orig_shape, Shape.cast(orig_shape),
                                          reset.shape()))
         else:
+            if reset is None:
+                reset = 0
             try:
-                reset = Const.cast(reset or 0)
+                reset = Const.cast(reset)
             except TypeError:
                 raise TypeError("Reset value must be a constant-castable expression, not {!r}"
                                 .format(orig_reset))
-        if orig_reset not in (None, 0, -1): # Avoid false positives for all-zeroes and all-ones
+        # Avoid false positives for all-zeroes and all-ones
+        if orig_reset is not None and not (isinstance(orig_reset, int) and orig_reset in (0, -1)):
             if reset.shape().signed and not self.signed:
                 warnings.warn(
                     message="Reset value {!r} is signed, but the signal shape is {!r}"
diff --git a/amaranth/hdl/ir.py b/amaranth/hdl/ir.py
index bd1c54f..e12d271 100644
--- a/amaranth/hdl/ir.py
+++ b/amaranth/hdl/ir.py
@@ -20,7 +20,7 @@ class UnusedElaboratable(UnusedMustUse):
     _MustUse__silence = True
 
 
-class Elaboratable(MustUse, metaclass=ABCMeta):
+class Elaboratable(MustUse):
     _MustUse__warning = UnusedElaboratable
 
 
diff --git a/amaranth/vendor/_gowin.py b/amaranth/vendor/_gowin.py
index 247664e..508d9c4 100644
--- a/amaranth/vendor/_gowin.py
+++ b/amaranth/vendor/_gowin.py
@@ -1,5 +1,6 @@
 from abc import abstractproperty
 from fractions import Fraction
+import math
 import re
 
 from ..hdl import *
@@ -160,19 +161,27 @@ class GowinPlatform(TemplatedPlatform):
 
     @property
     def _osc_div(self):
-        div_min  = 2
-        div_max  = 128
-        div_step = 2
-        div_frac = Fraction(self._osc_base_freq, self.osc_frequency)
-
-        if div_frac.denominator != 1 or div_frac not in range(div_min, div_max, div_step):
+        div_range = range(2, 130, 2)
+        div_frac  = Fraction(self._osc_base_freq, self.osc_frequency)
+
+        # Check that the requested frequency is within 50 ppm. This takes care of small mismatches
+        # arising due to rounding. The tolerance of a typical crystal oscillator is 50 ppm.
+        if (abs(round(div_frac) - div_frac) > Fraction(50, 1_000_000) or
+                int(div_frac) not in div_range):
+            achievable = (
+                min((frac for frac in div_range if frac > div_frac), default=None),
+                max((frac for frac in div_range if frac < div_frac), default=None)
+            )
             raise ValueError(
-                "On-chip oscillator frequency (platform.osc_frequency) must be chosen such that "
-                "the oscillator divider, calculated as ({}/{}), is an integer between {} and {} in "
-                "steps of {}"
-                .format(div_frac.numerator, div_frac.denominator, div_min, div_max, div_step))
+                f"On-chip oscillator frequency (platform.osc_frequency) must be chosen such that "
+                f"the base frequency of {self._osc_base_freq} Hz is divided by an integer factor "
+                f"between {div_range.start} and {div_range.stop} in steps of {div_range.step}; "
+                f"the divider for the requested frequency of {self.osc_frequency} Hz was "
+                f"calculated as ({div_frac.numerator}/{div_frac.denominator}), and the closest "
+                f"achievable frequencies are " +
+                ", ".join(str(self._osc_base_freq // frac) for frac in achievable if frac))
 
-        return div_frac.numerator
+        return int(div_frac)
 
     # Common templates
 
@@ -587,7 +596,7 @@ class GowinPlatform(TemplatedPlatform):
             )
         return m
 
-    def get_diff_input_output(self, pin, port, atttr, invert):
+    def get_diff_input_output(self, pin, port, attrs, invert):
         self._check_feature("differential input/output", pin, attrs,
                             valid_xdrs=(0, 1, 2), valid_attrs=True)
         m = Module()
diff --git a/amaranth/vendor/_lattice_ice40.py b/amaranth/vendor/_lattice_ice40.py
index c186988..790268b 100644
--- a/amaranth/vendor/_lattice_ice40.py
+++ b/amaranth/vendor/_lattice_ice40.py
@@ -160,6 +160,7 @@ class LatticeICE40Platform(TemplatedPlatform):
         r"""
         {{invoke_tool("icepack")}}
             {{verbose("-v")}}
+            {{get_override("icepack_opts")|options}}
             {{name}}.asc
             {{name}}.bin
         """
diff --git a/docs/_code/led_blinker.py b/docs/_code/led_blinker.py
index 986be2a..f42e1de 100644
--- a/docs/_code/led_blinker.py
+++ b/docs/_code/led_blinker.py
@@ -18,7 +18,7 @@ class LEDBlinker(Elaboratable):
 
         return m
 # --- BUILD ---
-from amaranth_boards.icestick import *
+from amaranth_boards.icestick import ICEStickPlatform
 
 
 ICEStickPlatform().build(LEDBlinker(), do_program=True)
diff --git a/docs/changes.rst b/docs/changes.rst
index 989ccda..6986ddb 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -125,6 +125,7 @@ Platform integration changes
 
 .. currentmodule:: amaranth.vendor
 
+* Added: ``icepack_opts`` override in :class:`vendor.LatticeICE40Platform`.
 * Added: ``OSCH`` as ``default_clk`` clock source in :class:`vendor.LatticeMachXO2Platform`, :class:`vendor.LatticeMachXO3LPlatform`.
 * Added: Xray toolchain support in :class:`vendor.XilinxPlatform`.
 * Added: :class:`vendor.GowinPlatform`.
diff --git a/examples/board/01_blinky.py b/examples/board/01_blinky.py
index 6e43ceb..4d0cb29 100644
--- a/examples/board/01_blinky.py
+++ b/examples/board/01_blinky.py
@@ -2,7 +2,7 @@
 # using the platform default clock (and default reset, if any).
 
 from amaranth import *
-from amaranth_boards.ice40_hx1k_blink_evn import *
+from amaranth_boards.ice40_hx1k_blink_evn import ICE40HX1KBlinkEVNPlatform
 
 
 class Blinky(Elaboratable):
diff --git a/examples/board/02_domain.py b/examples/board/02_domain.py
index 62ae565..294ac46 100644
--- a/examples/board/02_domain.py
+++ b/examples/board/02_domain.py
@@ -3,7 +3,7 @@
 # independently created in addition to the main "sync" domain.
 
 from amaranth import *
-from amaranth_boards.ice40_hx1k_blink_evn import *
+from amaranth_boards.ice40_hx1k_blink_evn import ICE40HX1KBlinkEVNPlatform
 
 
 class BlinkyWithDomain(Elaboratable):
diff --git a/pyproject.toml b/pyproject.toml
index 8e07f7f..8155b54 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -57,6 +57,9 @@ docs = [
   "sphinx-rtd-theme~=1.2",
   "sphinx-autobuild",
 ]
+examples = [
+  "amaranth-boards @ git+https://github.com/amaranth-lang/amaranth-boards.git"
+]
 
 [tool.pdm.scripts]
 test.composite = ["test-code", "test-docs"]


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 8745a53..c1ad320 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -1053,6 +1053,10 @@ class SignalTestCase(FHDLTestCase):
                 r"not <StringEnum\.FOO: 'a'>$"):
             Signal(1, reset=StringEnum.FOO)
 
+    def test_reset_const_castable(self):
+        s1 = Signal(4, reset=Cat(Const(0, 1), Const(1, 1), Const(0, 2)))
+        self.assertEqual(s1.reset, 2)
+
     def test_reset_shape_castable_const(self):
         class CastableFromHex(ShapeCastable):
             def as_shape(self):