Repository: amaranth-lang/amaranth (python)

Issue:
`Signal(range(0))` raises `SyntaxWarning` 
`Signal(range(0))` raises 
```
SyntaxWarning: Reset value 0 equals the non-inclusive end of the signal shape range(0, 0); this is likely an off-by-one error
```
Is this warning intended? 
To me this warning is quite confusing, as amaranth implicitly converts `range(0)` (which would be something like a never type) to `unsigned(0)`, and `Signal(unsigned(0))` does not raise the same warning. 

Some more discussion can be found in the [IRC logs](https://libera.irclog.whitequark.org/amaranth-lang/2024-01-07#1704643458-1704644743;).

Pull request:
hdl.ast: change warning on out-of-range reset to an error, improve it.
Fixes #1019.

Patch:
diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py
index 76643a0..9d4bb27 100644
--- a/amaranth/hdl/ast.py
+++ b/amaranth/hdl/ast.py
@@ -1170,13 +1170,14 @@ class Signal(Value, DUID, metaclass=_SignalMeta):
         self.reset = reset.value
         self.reset_less = bool(reset_less)
 
-        if isinstance(orig_shape, range) and self.reset == orig_shape.stop:
-            warnings.warn(
-                message="Reset value {!r} equals the non-inclusive end of the signal "
-                        "shape {!r}; this is likely an off-by-one error"
-                        .format(self.reset, orig_shape),
-                category=SyntaxWarning,
-                stacklevel=2)
+        if isinstance(orig_shape, range) and orig_reset is not None and orig_reset not in orig_shape:
+            if orig_reset == orig_shape.stop:
+                raise SyntaxError(
+                    f"Reset value {orig_reset!r} equals the non-inclusive end of the signal "
+                    f"shape {orig_shape!r}; this is likely an off-by-one error")
+            else:
+                raise SyntaxError(
+                    f"Reset value {orig_reset!r} is not within the signal shape {orig_shape!r}")
 
         self.attrs = OrderedDict(() if attrs is None else attrs)
 
diff --git a/docs/changes.rst b/docs/changes.rst
index 8735cee..9cdf331 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,6 +4,16 @@ Changelog
 This document describes changes to the public interfaces in the Amaranth language and standard library. It does not include most bug fixes or implementation changes.
 
 
+Documentation for past releases
+===============================
+
+Documentation for past releases of the Amaranth language and toolchain is available online:
+
+* `Amaranth 0.4.1 <https://amaranth-lang.org/docs/amaranth/v0.4.1/>`_
+* `Amaranth 0.4.0 <https://amaranth-lang.org/docs/amaranth/v0.4.0/>`_
+* `Amaranth 0.3 <https://amaranth-lang.org/docs/amaranth/v0.3/>`_
+
+
 Version 0.5 (unreleased)
 ========================
 
@@ -17,8 +27,8 @@ Apply the following changes to code written against Amaranth 0.4 to migrate it t
 
 * Replace uses of ``m.Case()`` with no patterns with ``m.Default()``
 * Replace uses of ``Value.matches()`` with no patterns with ``Const(1)``
-* Update uses of :func:`amaranth.utils.log2_int(need_pow2=False)` to :func:`amaranth.utils.ceil_log2`
-* Update uses of :func:`amaranth.utils.log2_int(need_pow2=True)` to :func:`amaranth.utils.exact_log2`
+* Update uses of ``amaranth.utils.log2_int(need_pow2=False)`` to :func:`amaranth.utils.ceil_log2`
+* Update uses of ``amaranth.utils.log2_int(need_pow2=True)`` to :func:`amaranth.utils.exact_log2`
 
 
 Implemented RFCs
@@ -40,6 +50,8 @@ Language changes
 * Added: :func:`amaranth.utils.ceil_log2`, :func:`amaranth.utils.exact_log2`. (`RFC 17`_)
 * Changed: ``m.Case()`` with no patterns is never active instead of always active. (`RFC 39`_)
 * 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.
 * Deprecated: :func:`amaranth.utils.log2_int`. (`RFC 17`_)
 * Removed: (deprecated in 0.4) :meth:`Const.normalize`. (`RFC 5`_)
 * Removed: (deprecated in 0.4) :class:`ast.Sample`, :class:`ast.Past`, :class:`ast.Stable`, :class:`ast.Rose`, :class:`ast.Fell`.
@@ -63,7 +75,7 @@ Platform integration changes
 * Added: :meth:`BuildPlan.execute_local_docker`.
 * Added: :meth:`BuildPlan.extract`.
 * Added: ``build.sh``  begins with ``#!/bin/sh``.
-* Deprecated: argument `run_script=` in :meth:`BuildPlan.execute_local`
+* Deprecated: argument ``run_script=`` in :meth:`BuildPlan.execute_local`.
 * Removed: (deprecated in 0.4) :mod:`vendor.intel`, :mod:`vendor.lattice_ecp5`, :mod:`vendor.lattice_ice40`, :mod:`vendor.lattice_machxo2_3l`, :mod:`vendor.quicklogic`, :mod:`vendor.xilinx`. (`RFC 18`_)
 
 


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 3333a5a..1af21f2 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -1106,10 +1106,8 @@ class SignalTestCase(FHDLTestCase):
         self.assertEqual(s8.shape(), signed(5))
         s9 = Signal(range(-20, 16))
         self.assertEqual(s9.shape(), signed(6))
-        with warnings.catch_warnings():
-            warnings.filterwarnings(action="ignore", category=SyntaxWarning)
-            s10 = Signal(range(0))
-            self.assertEqual(s10.shape(), unsigned(0))
+        s10 = Signal(range(0))
+        self.assertEqual(s10.shape(), unsigned(0))
         s11 = Signal(range(1))
         self.assertEqual(s11.shape(), unsigned(1))
 
@@ -1184,10 +1182,22 @@ class SignalTestCase(FHDLTestCase):
             Signal(signed(1), reset=-2)
 
     def test_reset_wrong_fencepost(self):
-        with self.assertWarnsRegex(SyntaxWarning,
+        with self.assertRaisesRegex(SyntaxError,
                 r"^Reset value 10 equals the non-inclusive end of the signal shape "
                 r"range\(0, 10\); this is likely an off-by-one error$"):
             Signal(range(0, 10), reset=10)
+        with self.assertRaisesRegex(SyntaxError,
+                r"^Reset value 0 equals the non-inclusive end of the signal shape "
+                r"range\(0, 0\); this is likely an off-by-one error$"):
+            Signal(range(0), reset=0)
+
+    def test_reset_wrong_range(self):
+        with self.assertRaisesRegex(SyntaxError,
+                r"^Reset value 11 is not within the signal shape range\(0, 10\)$"):
+            Signal(range(0, 10), reset=11)
+        with self.assertRaisesRegex(SyntaxError,
+                r"^Reset value 0 is not within the signal shape range\(1, 10\)$"):
+            Signal(range(1, 10), reset=0)
 
     def test_attrs(self):
         s1 = Signal()