hdl.ast: implement ValueCastable.
authorawygle <awygle@gmail.com>
Fri, 6 Nov 2020 00:20:54 +0000 (16:20 -0800)
committerGitHub <noreply@github.com>
Fri, 6 Nov 2020 00:20:54 +0000 (00:20 +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
 from abc import ABCMeta, abstractmethod
 import traceback
+import sys
 import warnings
 import typing
 import warnings
 import typing
+import functools
 from collections import OrderedDict
 from collections.abc import Iterable, MutableMapping, MutableSet, MutableSequence
 from enum import Enum
 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",
     "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",
     "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)))
             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):
         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()
 
 
         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.
 @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)
 
 
         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")
 class SampleTestCase(FHDLTestCase):
     def test_const(self):
         s = Sample(1, 1, "sync")