Repository: amaranth-lang/amaranth (python)

Issue:
Slices resulting from `.rotate_{left,right}` are not considered constant-castable
Previously, in #911, I noticed that you could not use a Const() as a reset= argument when creating a Signal. The reason why I wanted this was to make a value with a particular shape.

This has been fixed, but today I noticed that you cannot use a Const() with a mathematical operation on it, such as rotate_right(). The reason I want this is that there is no equivalent to rotate_right() for numbers in the Python standard library, only shift.

## Repro

Consider the branch `z_bug_cellular-toy-consts` of [this repo](https://github.com/mcclure/analogue-core-template-amaranth-variants/tree/z_bug_cellular-toy-consts).

On [this line](https://github.com/mcclure/analogue-core-template-amaranth-variants/blob/z_bug_cellular-toy-consts/src/fpga/amaranth_core/embed_amaranth_core/toplevel.py#L34) I say

    clk_reg = Signal(self.ratio, reset=Const((1 << (self.ratio // 2)) - 1, unsigned(self.ratio)).rotate_right(self.ratio//2-1))

At execution time "self.ratio" is equal to 60. So I want to construct the value caused by filling half the bits in a 60-bit register, then right-rotating it a bit.

If you build this with `(cd src/fpga/amaranth_core/ && python3 -m pdm install && python3 -m pdm generate)`, you get

```
TypeError: Value (slice (const 60'd1073741823) 29:60) cannot be converted to an Amaranth constant
...
TypeError: Reset value must be a constant-castable expression, not (cat (slice (const 60'd1073741823) 29:60) (slice (const 60'd1073741823) 0:29))
```

If you remove the `.rotate_right(self.ratio//2-1)` this line works fine, and a few lines later I am able to `print` the reset value of clk_reg:

```
Clocks 1073741823,32767; stb 288230376151711744
```
 
I am testing with amaranth-yosys 0.35 and Python 3.11.6.

## Expected behavior / could this be fixed another way?

At a high level, my "expected behavior" is that "reasonable" constant Amaranth expressions, such as `Const(43).rotate_left(n)`, `Const(43) + 45` etc should be allowed as `reset=` arguments.

It might be that evaluating constness is nontrivial and that this is not an easy feature to implement. In this case, I would expect the constrained set of expressions which *are* allowed to be documented. I would also expect this low-level/practical thing: *I would expect there to be some way to rotate_left and rotate_right numbers and assign them to `reset=` without using libraries other than Amaranth`. There are several ways this could be achieved:

- Make `reset=` accept `Cat` objects, special-cased, since it is easy to confirm that a `Cat` is a tree of only `Cat` and `Const` expressions.
- Make the `.value` property, documented on `Const`, work on constant `Cat` objects. (Right now this gives the error: `AttributeError: 'Cat' object has no attribute 'value'`.) Then I could call `.value` and assign that to `reset=`.
- Offer rotate-left and rotate-right functions that operate on regular Python integers. They can keep the power-of-two functions in `util` company!

For now, I will possibly be either implementing it myself (it is not hard to implement but it is nice to be able to exclude the math functions as sources of bugs, which is harder if the math functions were implemented bespoke for my program…) or using the "bitstring" library.

Pull request:
hdl.ast: make `Slice` const-castable.
Fixes #1006.

Patch:
diff --git a/amaranth/hdl/ast.py b/amaranth/hdl/ast.py
index 2ed8801..70e4763 100644
--- a/amaranth/hdl/ast.py
+++ b/amaranth/hdl/ast.py
@@ -727,9 +727,13 @@ class Const(Value):
             width = 0
             for part in obj.parts:
                 const  = Const.cast(part)
-                value |= const.value << width
+                part_value = Const(const.value, unsigned(const.width)).value
+                value |= part_value << width
                 width += len(const)
             return Const(value, width)
+        elif type(obj) is Slice:
+            value = Const.cast(obj.value)
+            return Const(value.value >> obj.start, unsigned(obj.stop - obj.start))
         else:
             raise TypeError(f"Value {obj!r} cannot be converted to an Amaranth constant")
 
diff --git a/docs/changes.rst b/docs/changes.rst
index 6b7e752..34a251c 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -15,6 +15,7 @@ Language changes
 
 .. currentmodule:: amaranth.hdl
 
+* Added: `class:ast.Slice` objects have been made const-castable.
 * 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`.
 
diff --git a/docs/lang.rst b/docs/lang.rst
index fdd197b..d238050 100644
--- a/docs/lang.rst
+++ b/docs/lang.rst
@@ -325,6 +325,7 @@ They may also be provided as a pattern to the :ref:`match operator <lang-matchop
 
    * :class:`Const`
    * :class:`Cat`
+   * :class:`Slice`
 
    This list will be expanded in the future.
 


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 625e0e8..b97b2a1 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -799,6 +799,16 @@ class SliceTestCase(FHDLTestCase):
         s1 = Const(10)[2]
         self.assertEqual(repr(s1), "(slice (const 4'd10) 2:3)")
 
+    def test_const(self):
+        a = Const.cast(Const(0x1234, 16)[4:12])
+        self.assertEqual(a.value, 0x23)
+        self.assertEqual(a.width, 8)
+        self.assertEqual(a.signed, False)
+        a = Const.cast(Const(-4, signed(8))[1:6])
+        self.assertEqual(a.value, 0x1e)
+        self.assertEqual(a.width, 5)
+        self.assertEqual(a.signed, False)
+
 
 class BitSelectTestCase(FHDLTestCase):
     def setUp(self):
@@ -935,6 +945,16 @@ class CatTestCase(FHDLTestCase):
                 r"specify the width explicitly using C\(2, 2\)$"):
             Cat(2)
 
+    def test_const(self):
+        a = Const.cast(Cat(Const(1, 1), Const(0, 1), Const(3, 2), Const(2, 2)))
+        self.assertEqual(a.value, 0x2d)
+        self.assertEqual(a.width, 6)
+        self.assertEqual(a.signed, False)
+        a = Const.cast(Cat(Const(-4, 8), Const(-3, 8)))
+        self.assertEqual(a.value, 0xfdfc)
+        self.assertEqual(a.width, 16)
+        self.assertEqual(a.signed, False)
+
 
 class ReplTestCase(FHDLTestCase):
     @_ignore_deprecated
@@ -1187,6 +1207,12 @@ class SignalTestCase(FHDLTestCase):
         self.assertEqual(s2.shape(), signed(2))
         self.assertEqual(s2.decoder(SignedEnum.FOO), "FOO/-1")
 
+    def test_const_wrong(self):
+        s1 = Signal()
+        with self.assertRaisesRegex(TypeError,
+                r"^Value \(sig s1\) cannot be converted to an Amaranth constant$"):
+            Const.cast(s1)
+
 
 class ClockSignalTestCase(FHDLTestCase):
     def test_domain(self):