From 06c734992fd46b6f11bdfa4425ef102640aecf3f Mon Sep 17 00:00:00 2001 From: awygle Date: Thu, 5 Nov 2020 16:20:54 -0800 Subject: [PATCH] hdl.ast: implement ValueCastable. Closes RFC issue #355. --- nmigen/hdl/ast.py | 51 ++++++++++++++++++++++++++++++++++++++++++- tests/test_hdl_ast.py | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/nmigen/hdl/ast.py b/nmigen/hdl/ast.py index 4a01bde..0b86e71 100644 --- a/nmigen/hdl/ast.py +++ b/nmigen/hdl/ast.py @@ -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. diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py index e6dfc49..b0d40d5 100644 --- a/tests/test_hdl_ast.py +++ b/tests/test_hdl_ast.py @@ -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") -- 2.30.2