hdl.ast: implement ValueCastable.
authorawygle <awygle@gmail.com>
Fri, 6 Nov 2020 00:20:54 +0000 (16:20 -0800)
committerLuke Kenneth Casson Leighton <lkcl@lkcl.net>
Fri, 31 Dec 2021 15:19:31 +0000 (15:19 +0000)
Closes RFC issue #355.

nmigen/hdl/ast.py
tests/test_hdl_ast.py

index 4a01bde32b2da347d153fd3804ee629e68c7fcbf..0b86e7153da0cac1dd5d62c83cca15caaf7357e2 100644 (file)
@@ -1,7 +1,9 @@
 from abc import ABCMeta, abstractmethod
 import traceback
+import sys
 import warnings
 import typing
+import functools
 from collections import OrderedDict
 from collections.abc import Iterable, MutableMapping, MutableSet, MutableSequence
 from enum import Enum
@@ -16,7 +18,7 @@ __all__ = [
     "Value", "Const", "C", "AnyConst", "AnySeq", "Operator", "Mux", "Part", "Slice", "Cat", "Repl",
     "Array", "ArrayProxy",
     "Signal", "ClockSignal", "ResetSignal",
-    "UserValue",
+    "UserValue", "ValueCastable",
     "Sample", "Past", "Stable", "Rose", "Fell", "Initial",
     "Statement", "Switch",
     "Property", "Assign", "Assert", "Assume", "Cover",
@@ -142,6 +144,8 @@ class Value(metaclass=ABCMeta):
             return Const(obj)
         if isinstance(obj, Enum):
             return Const(obj.value, Shape.cast(type(obj)))
+        if isinstance(obj, ValueCastable):
+            return obj.as_value()
         raise TypeError("Object {!r} cannot be converted to an nMigen value".format(obj))
 
     def __init__(self, *, src_loc_at=0):
@@ -1280,6 +1284,51 @@ class UserValue(Value):
         return self._lazy_lower()._rhs_signals()
 
 
+class ValueCastable:
+    """Base class for classes which can be cast to Values.
+
+    A ``ValueCastable`` can be cast to ``Value``, meaning its precise representation does not have
+    to be immediately known. This is useful in certain metaprogramming scenarios. Instead of
+    providing fixed semantics upfront, it is kept abstract for as long as possible, only being
+    cast to a concrete nMigen value when required.
+
+    Note that it is necessary to ensure that nMigen's view of representation of all values stays 
+    internally consistent. The class deriving from ``ValueCastable`` must decorate the ``as_value``
+    method with the ``lowermethod`` decorator, which ensures that all calls to ``as_value``return the
+    same ``Value`` representation. If the class deriving from ``ValueCastable`` is mutable, it is
+    up to the user to ensure that it is not mutated in a way that changes its representation after
+    the first call to ``as_value``.
+    """
+    def __new__(cls, *args, **kwargs):
+        self = super().__new__(cls)
+        if not hasattr(self, "as_value"):
+            raise TypeError(f"Class '{cls.__name__}' deriving from `ValueCastable` must override the `as_value` method")
+
+        if not hasattr(self.as_value, "_ValueCastable__memoized"):
+            raise TypeError(f"Class '{cls.__name__}' deriving from `ValueCastable` must decorate the `as_value` "
+                            "method with the `ValueCastable.lowermethod` decorator")
+        return self
+
+    @staticmethod
+    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 ``as_value`` method of ``ValueCastable`` subclasses.
+        This is to ensure that nMigen's view of representation of all values stays internally
+        consistent.
+        """
+        @functools.wraps(func)
+        def wrapper_memoized(self, *args, **kwargs):
+            if not hasattr(self, "_ValueCastable__lowered_to"):
+                self.__lowered_to = func(self, *args, **kwargs)
+            return self.__lowered_to
+        wrapper_memoized.__memoized = True
+        return wrapper_memoized
+
+
 @final
 class Sample(Value):
     """Value from the past.
index e6dfc49e081f0cdf5d79fc932d1d6e3ff0174116..b0d40d5c6beee395256ce2459766f9fc9e0a2d50 100644 (file)
@@ -1025,6 +1025,52 @@ class UserValueTestCase(FHDLTestCase):
         self.assertEqual(uv.lower_count, 1)
 
 
+class MockValueCastableChanges(ValueCastable):
+    def __init__(self, width=0):
+        self.width = width
+
+    @ValueCastable.lowermethod
+    def as_value(self):
+        return Signal(self.width)
+
+
+class MockValueCastableNotDecorated(ValueCastable):
+    def __init__(self):
+        pass
+
+    def as_value(self):
+        return Signal()
+
+
+class MockValueCastableNoOverride(ValueCastable):
+    def __init__(self):
+        pass
+
+
+class ValueCastableTestCase(FHDLTestCase):
+    def test_not_decorated(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Class 'MockValueCastableNotDecorated' deriving from `ValueCastable` must decorate the `as_value` "
+                r"method with the `ValueCastable.lowermethod` decorator$"):
+            vc = MockValueCastableNotDecorated()
+
+    def test_no_override(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Class 'MockValueCastableNoOverride' deriving from `ValueCastable` must override the `as_value` "
+                r"method$"):
+            vc = MockValueCastableNoOverride()
+
+    def test_memoized(self):
+        vc = MockValueCastableChanges(1)
+        sig1 = vc.as_value()
+        vc.width = 2
+        sig2 = vc.as_value()
+        self.assertIs(sig1, sig2)
+        vc.width = 3
+        sig3 = Value.cast(vc)
+        self.assertIs(sig1, sig3)
+
+
 class SampleTestCase(FHDLTestCase):
     def test_const(self):
         s = Sample(1, 1, "sync")