Repository: amaranth-lang/amaranth
Pull request #1205 (2024-03-15 10:40:47)

Backport bug fixes to v0.4.x

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1205?dropdown=coverage&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:
> Project coverage is 83.86%. Comparing base [(`a430c1d`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/a430c1d4a37c07328d30e37de15d077c90dc0aa5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) to head [(`044efe5`)](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1205?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).


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


```diff
@@            Coverage Diff             @@
##           v0.4.x    #1205      +/-   ##
==========================================
+ Coverage   83.77%   83.86%   +0.09%     
==========================================
  Files          55       55              
  Lines        8129     8127       -2     
  Branches     1985     1984       -1     
==========================================
+ Hits         6810     6816       +6     
+ Misses       1106     1098       -8     
  Partials      213      213              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1205?dropdown=coverage&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/.github/workflows/main.yaml b/.github/workflows/main.yaml
index d245b10..fc52291 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -24,15 +24,15 @@ jobs:
         include:
         - python-version: '3.13-dev'
           allow-failure: true
-    continue-on-error: '${{ matrix.allow-failure }}'
+    continue-on-error: ${{ matrix.allow-failure }}
     name: 'test (${{ matrix.python-version }})'
     steps:
     - name: Check out source code
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
       with:
         fetch-depth: 0
     - name: Set up PDM
-      uses: pdm-project/setup-pdm@v3
+      uses: pdm-project/setup-pdm@v4
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
@@ -41,10 +41,10 @@ jobs:
         sudo add-apt-repository 'deb http://ppa.launchpad.net/sri-csl/formal-methods/ubuntu bionic main'
         sudo apt-get update
         sudo apt-get install yices2
-        pip install codecov
+        pip install codecov build
         pdm install --dev
     - name: Cache YoWASP build products
-      uses: actions/cache@v3
+      uses: actions/cache@v4
       with:
         path: ~/.cache/YoWASP
         key: YoWASP-${{ runner.os }}-${{ hashFiles('./.venv/**/*.wasm') }}
@@ -54,21 +54,21 @@ jobs:
       run: |
         pdm run test
     - name: Submit code coverage
-      run:
+      run: |
         codecov
 
   document:
     runs-on: ubuntu-latest
     steps:
       - name: Check out source code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
       - name: Fetch tags from upstream repository
         run: |
           git fetch --tags https://github.com/amaranth-lang/amaranth.git
       - name: Set up PDM
-        uses: pdm-project/setup-pdm@v3
+        uses: pdm-project/setup-pdm@v4
         with:
           python-version: '3.11'
       - name: Install dependencies
@@ -78,7 +78,7 @@ jobs:
         run: |
           pdm run document
       - name: Upload documentation archive
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: docs
           path: docs/_build
@@ -87,27 +87,27 @@ jobs:
     needs:
       - test
       - document
-    if: always() && !contains(needs.*.result, 'cancelled')
+    if: ${{ always() && !contains(needs.*.result, 'cancelled') }}
     runs-on: ubuntu-latest
     steps:
       - run: ${{ contains(needs.*.result, 'failure') && 'false' || 'true' }}
 
   publish-docs:
     needs: document
-    if: github.repository == 'amaranth-lang/amaranth'
+    if: ${{ github.repository == 'amaranth-lang/amaranth' }}
     runs-on: ubuntu-latest
     steps:
       - name: Check out source code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
       - name: Download documentation archive
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: docs
           path: docs/
       - name: Publish development documentation
-        if: github.event_name == 'push' && github.event.ref == 'refs/heads/main'
+        if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' }}
         uses: JamesIves/github-pages-deploy-action@releases/v4
         with:
           repository-name: amaranth-lang/amaranth-lang.github.io
@@ -116,7 +116,7 @@ jobs:
           folder: docs/
           target-folder: docs/amaranth/latest/
       - name: Publish release documentation
-        if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
+        if: ${{ github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') }}
         uses: JamesIves/github-pages-deploy-action@releases/v4
         with:
           repository-name: amaranth-lang/amaranth-lang.github.io
@@ -127,15 +127,15 @@ jobs:
 
   publish-docs-dev:
     needs: document
-    if: github.repository != 'amaranth-lang/amaranth'
+    if: ${{ github.repository != 'amaranth-lang/amaranth' }}
     runs-on: ubuntu-latest
     steps:
       - name: Check out source code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
       - name: Download documentation archive
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: docs
           path: pages/docs/${{ github.ref_name }}/
@@ -147,3 +147,57 @@ jobs:
         with:
           folder: pages/
           clean: false
+
+  publish-package:
+    needs: required
+    runs-on: ubuntu-latest
+    environment: publish
+    permissions:
+      id-token: write
+    steps:
+    - name: Check out source code
+      uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+    - name: Build package
+      run: |
+        pip install build
+        python -m build
+    - name: Upload package artifact
+      uses: actions/upload-artifact@v4
+      with:
+        name: package
+        path: dist/
+    - name: Verify package metadata
+      run: |
+        pip install twine
+        twine check dist/*
+    - name: Publish package to Test PyPI
+      if: ${{ github.repository == 'amaranth-lang/amaranth' && github.event_name == 'push' && github.event.ref == 'refs/heads/main' }}
+      uses: pypa/gh-action-pypi-publish@release/v1
+      with:
+        repository-url: https://test.pypi.org/legacy/
+    - name: Publish package to PyPI
+      if: ${{ github.repository == 'amaranth-lang/amaranth' && github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') }}
+      uses: pypa/gh-action-pypi-publish@release/v1
+
+  publish-release:
+    needs: publish-package
+    if: ${{ github.repository == 'amaranth-lang/amaranth' && github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') }}
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+    steps:
+    - name: Determine release metadata
+      id: metadata
+      env:
+        REF_NAME: ${{ github.ref_name }}
+      run: |
+        echo name=${REF_NAME/v/} >>$GITHUB_OUTPUT
+    - name: Create release
+      uses: comnoco/create-release-action@v2
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      with:
+        tag_name: ${{ github.ref_name }}
+        release_name: ${{ steps.metadata.outputs.name }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 6f0d53d..97b9781 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,11 @@ See the [Introduction](https://amaranth-lang.org/docs/amaranth/latest/intro.html
 
 See the [Installation](https://amaranth-lang.org/docs/amaranth/latest/install.html) section of the documentation.
 
-## Supported devices
+## Usage
+
+See the [Language guide](https://amaranth-lang.org/docs/amaranth/latest/guide.html) section of the documentation.
+
+## Platform support
 
 Amaranth can be used to target any FPGA or ASIC process that accepts behavioral Verilog-2001 as input. It also offers extended support for many FPGA families, providing toolchain integration, abstractions for device-specific primitives, and more. Specifically:
 
@@ -30,10 +34,11 @@ Amaranth can be used to target any FPGA or ASIC process that accepts behavioral
   * Lattice MachXO2 (toolchains: Diamond);
   * Lattice MachXO3L (toolchains: Diamond);
   * Lattice ECP5 (toolchains: **Yosys+nextpnr**, Diamond);
-  * Xilinx Spartan 3A (toolchains: ISE);
-  * Xilinx Spartan 6 (toolchains: ISE);
-  * Xilinx 7-series (toolchains: Vivado);
-  * Xilinx UltraScale (toolchains: Vivado);
+  * Lattice Nexus (toolchains: **Yosys+nextpnr**, Diamond);
+  * AMD Spartan 3A (toolchains: ISE);
+  * AMD Spartan 6 (toolchains: ISE);
+  * AMD 7-series (toolchains: Vivado);
+  * AMD UltraScale (toolchains: Vivado);
   * Intel (toolchains: Quartus);
   * Quicklogic EOS S3 (toolchains: **Yosys+VPR**).
 
@@ -47,4 +52,4 @@ Amaranth has a dedicated IRC channel, [#amaranth-lang at libera.chat](https://we
 
 ## License
 
-Amaranth is released under the very permissive [two-clause BSD license](LICENSE.txt). Under the terms of this license, you are authorized to use Amaranth for closed-source proprietary designs.
+Amaranth is released under the [two-clause BSD license](LICENSE.txt). You are permitted to use Amaranth for open-source and proprietary designs provided that the copyright notice in the license file is reproduced.
diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py
index f54c076..1b612c4 100644
--- a/amaranth/lib/wiring.py
+++ b/amaranth/lib/wiring.py
@@ -772,8 +772,6 @@ class Signature(metaclass=SignatureMeta):
                     yield path, Member(member.flow, member.shape, reset=member.reset), value
                 elif member.is_signature:
                     for sub_path, sub_member, sub_value in member.signature.flatten(value):
-                        if member.flow == In:
-                            sub_member = sub_member.flip()
                         yield ((*path, *sub_path), sub_member, sub_value)
                 else:
                     assert False # :nocov:
diff --git a/amaranth/utils.py b/amaranth/utils.py
index 4f25af6..55eb66c 100644
--- a/amaranth/utils.py
+++ b/amaranth/utils.py
@@ -9,7 +9,7 @@ def ceil_log2(n):
     Raises a `ValueError` for negative inputs."""
     n = operator.index(n)
     if n < 0:
-        raise ValueError("{n} is negative")
+        raise ValueError(f"{n} is negative")
     if n == 0:
         return 0
     return (n - 1).bit_length()
@@ -21,7 +21,7 @@ def exact_log2(n):
     Raises a `ValueError` if `n` is not a power of two."""
     n = operator.index(n)
     if n <= 0 or (n & (n - 1)):
-        raise ValueError("{n} is not a power of 2")
+        raise ValueError(f"{n} is not a power of 2")
     return (n - 1).bit_length()
 
 def log2_int(n, need_pow2=True):
diff --git a/pyproject.toml b/pyproject.toml
index b44683e..de17f1a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,6 +8,7 @@ dynamic = ["version"]
 
 name = "amaranth"
 description = "Amaranth hardware definition language"
+readme = "README.md"
 authors = [{name = "Amaranth HDL contributors"}]
 license = {file = "LICENSE.txt"}
 


Test patch:
diff --git a/tests/test_lib_wiring.py b/tests/test_lib_wiring.py
index 206fa88..da2c8ec 100644
--- a/tests/test_lib_wiring.py
+++ b/tests/test_lib_wiring.py
@@ -349,8 +349,8 @@ class SignatureTestCase(unittest.TestCase):
         self.assertFlattenedSignature(sig.flatten(intf), [
             (("a", "p"), Out(1), intf.a.p),
             (("b", "q"), In (1), intf.b.q),
-            (("c", "r"), Out(1), intf.c.r),
-            (("d", "s"), In (1), intf.d.s),
+            (("c", "r"), In (1), intf.c.r),
+            (("d", "s"), Out(1), intf.d.s),
         ])
 
     def test_is_compliant_signature(self):
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..608336d
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,71 @@
+import unittest
+
+from amaranth.utils import *
+from amaranth._utils import _ignore_deprecated
+
+
+class Log2TestCase(unittest.TestCase):
+    def test_ceil_log2(self):
+        self.assertEqual(ceil_log2(0), 0)
+        self.assertEqual(ceil_log2(1), 0)
+        self.assertEqual(ceil_log2(2), 1)
+        self.assertEqual(ceil_log2(3), 2)
+        self.assertEqual(ceil_log2(4), 2)
+        self.assertEqual(ceil_log2(5), 3)
+        self.assertEqual(ceil_log2(8), 3)
+        self.assertEqual(ceil_log2(9), 4)
+        with self.assertRaises(TypeError):
+            ceil_log2(1.5)
+        with self.assertRaisesRegex(ValueError, r"^-1 is negative$"):
+            ceil_log2(-1)
+
+    def test_exact_log2(self):
+        self.assertEqual(exact_log2(1), 0)
+        self.assertEqual(exact_log2(2), 1)
+        self.assertEqual(exact_log2(4), 2)
+        self.assertEqual(exact_log2(8), 3)
+        for val in [-1, 0, 3, 5, 6, 7, 9]:
+            with self.assertRaisesRegex(ValueError, (f"^{val} is not a power of 2$")):
+                exact_log2(val)
+        with self.assertRaises(TypeError):
+            exact_log2(1.5)
+
+    @_ignore_deprecated
+    def test_log2_int(self):
+        self.assertEqual(log2_int(1), 0)
+        self.assertEqual(log2_int(2), 1)
+        self.assertEqual(log2_int(4), 2)
+        self.assertEqual(log2_int(8), 3)
+        for val in [-1, 3, 5, 6, 7, 9]:
+            with self.assertRaises(ValueError):
+                log2_int(val)
+        self.assertEqual(log2_int(0, False), 0)
+        self.assertEqual(log2_int(1, False), 0)
+        self.assertEqual(log2_int(2, False), 1)
+        self.assertEqual(log2_int(3, False), 2)
+        self.assertEqual(log2_int(4, False), 2)
+        self.assertEqual(log2_int(5, False), 3)
+        self.assertEqual(log2_int(8, False), 3)
+        self.assertEqual(log2_int(9, False), 4)
+
+    def test_bits_for(self):
+        self.assertEqual(bits_for(-4), 3)
+        self.assertEqual(bits_for(-3), 3)
+        self.assertEqual(bits_for(-2), 2)
+        self.assertEqual(bits_for(-1), 1)
+        self.assertEqual(bits_for(0), 1)
+        self.assertEqual(bits_for(1), 1)
+        self.assertEqual(bits_for(2), 2)
+        self.assertEqual(bits_for(3), 2)
+        self.assertEqual(bits_for(4), 3)
+        self.assertEqual(bits_for(5), 3)
+        self.assertEqual(bits_for(-4, True), 3)
+        self.assertEqual(bits_for(-3, True), 3)
+        self.assertEqual(bits_for(-2, True), 2)
+        self.assertEqual(bits_for(-1, True), 1)
+        self.assertEqual(bits_for(0, True), 1)
+        self.assertEqual(bits_for(1, True), 2)
+        self.assertEqual(bits_for(2, True), 3)
+        self.assertEqual(bits_for(3, True), 3)
+        self.assertEqual(bits_for(4, True), 4)
+        self.assertEqual(bits_for(5, True), 4)