Repository: amaranth-lang/amaranth
Pull request #1202 (2024-03-14 19:09:08)

Implement RFC 51: Add `ShapeCastable.from_bits` and `amaranth.lib.data.Const`.
TODO:

- [x] tests
- [x] documentation
- [ ] ~~add custom `Const` subclasses for `Struct`/`Union`?~~ deferred

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1202?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
Attention: Patch coverage is `52.67857%` with `53 lines` in your changes are missing coverage. Please review.
> Project coverage is 88.89%. Comparing base [(`3e6e780`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/3e6e78012d596d6e8154eefd4bd4cc95efd58339?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) to head [(`c0ccc99`)](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1202?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).

> :exclamation: Current head c0ccc99 differs from pull request most recent head 88f603e. Consider uploading reports for the commit 88f603e to get more accurate results

| [Files](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1202?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) | Patch % | Lines |
|---|---|---|
| [amaranth/lib/data.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1202?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvbGliL2RhdGEucHk=) | 50.94% | [48 Missing and 4 partials :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1202?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |
| [amaranth/lib/enum.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1202?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvbGliL2VudW0ucHk=) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1202?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1202      +/-   ##
==========================================
- Coverage   89.44%   88.89%   -0.55%     
==========================================
  Files          43       43              
  Lines        9069     9170     +101     
  Branches     2151     2169      +18     
==========================================
+ Hits         8112     8152      +40     
- Misses        780      835      +55     
- Partials      177      183       +6     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1202?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).


Patch:
diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py
index dc4d3d4..e8a6553 100644
--- a/amaranth/hdl/_ast.py
+++ b/amaranth/hdl/_ast.py
@@ -243,6 +243,10 @@ class ShapeCastable:
         if cls.__call__ is ShapeCastable.__call__:
             raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override "
                             f"the '__call__' method")
+        if cls.from_bits is ShapeCastable.from_bits:
+            warnings.warn(f"Class '{cls.__name__}' deriving from 'ShapeCastable' does not override "
+                          f"the 'from_bits' method, which will be required in Amaranth 0.6",
+                          DeprecationWarning, stacklevel=2)
 
     # The signatures and definitions of these methods are weird because they are present here for
     # documentation (and error checking above) purpose only and should not affect control flow.
@@ -308,6 +312,40 @@ class ShapeCastable:
         """
         return super().const(*args, **kwargs) # :nocov:
 
+    def from_bits(self, raw):
+        """Lift a bit pattern to a higher-level representation.
+
+        This method is called by the Amaranth language to lift :py:`raw`, which is an :class:`int`,
+        to a higher-level representation, which may be any object accepted by :meth:`const`.
+        Most importantly, the simulator calls this method when the value of a shape-castable
+        object is retrieved.
+
+        For any valid bit pattern :py:`raw`, the following condition must hold:
+
+        .. code::
+
+            Const.cast(self.const(self.from_bits(raw))).value == raw
+
+        While :meth:`const` will usually return an Amaranth value or a custom value-castable
+        object that is convenient to use while constructing the design, this method will usually
+        return a Python object that is convenient to use while simulating the design. While not
+        constrained here, these objects should have the same type whenever feasible.
+
+        This method may also be called by code that is not a part of the Amaranth language.
+
+        Returns
+        -------
+        unspecified type
+
+        Raises
+        ------
+        Exception
+            When the bit pattern isn't valid. This exception must be propagated by callers,
+            either directly or as a cause of another exception. While not constrained here,
+            usually the exception class will be :exc:`ValueError`.
+        """
+        return raw
+
     def __call__(self, *args, **kwargs):
         """__call__(obj)
 
diff --git a/amaranth/lib/data.py b/amaranth/lib/data.py
index afd775e..a3e6199 100644
--- a/amaranth/lib/data.py
+++ b/amaranth/lib/data.py
@@ -2,16 +2,17 @@ from abc import ABCMeta, abstractmethod
 from enum import Enum
 from collections.abc import Mapping, Sequence
 import warnings
+import operator
 
 from amaranth._utils import final
 from amaranth.hdl import *
 from amaranth.hdl._repr import *
-from amaranth.hdl._ast import ShapeCastable, ValueCastable
+from amaranth import hdl
 
 
 __all__ = [
     "Field", "Layout", "StructLayout", "UnionLayout", "ArrayLayout", "FlexibleLayout",
-    "View", "Struct", "Union",
+    "View", "Const", "Struct", "Union",
 ]
 
 
@@ -26,7 +27,7 @@ class Field:
 
     Attributes
     ----------
-    shape : :ref:`shape-like <lang-shapelike>`
+    shape : :class:`.ShapeLike`
         Shape of the field. When initialized or assigned, the object is stored as-is.
     offset : :class:`int`, >=0
         Index of the least significant bit of the field.
@@ -55,14 +56,14 @@ class Field:
     def width(self):
         """Width of the field.
 
-        This property should be used over ``self.shape.width`` because ``self.shape`` can be
+        This property should be used over :py:`self.shape.width` because :py:`self.shape` can be
         an arbitrary :ref:`shape-like <lang-shapelike>` object, which may not have
-        a ``width`` property.
+        a :py:`width` property.
 
         Returns
         -------
         :class:`int`
-            ``Shape.cast(self.shape).width``
+            :py:`Shape.cast(self.shape).width`
         """
         return Shape.cast(self.shape).width
 
@@ -98,15 +99,15 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
     def cast(obj):
         """Cast a :ref:`shape-like <lang-shapelike>` object to a layout.
 
-        This method performs a subset of the operations done by :meth:`Shape.cast`; it will
-        recursively call ``.as_shape()``, but only until a layout is returned.
+        This method performs a subset of the operations done by :meth:`.Shape.cast`; it will
+        recursively call :py:`.as_shape()`, but only until a layout is returned.
 
         Raises
         ------
         TypeError
-            If ``obj`` cannot be converted to a :class:`Layout` instance.
+            If :py:`obj` cannot be converted to a :class:`Layout` instance.
         RecursionError
-            If ``obj.as_shape()`` returns ``obj``.
+            If :py:`obj.as_shape()` returns :py:`obj`.
         """
         while isinstance(obj, ShapeCastable):
             if isinstance(obj, Layout):
@@ -138,12 +139,12 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
         Returns
         -------
         :class:`Field`
-            The field associated with ``key``.
+            The field associated with :py:`key`.
 
         Raises
         ------
         KeyError
-            If there is no field associated with ``key``.
+            If there is no field associated with :py:`key`.
         """
 
     @property
@@ -162,8 +163,8 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
 
         Returns
         -------
-        :class:`Shape`
-            ``unsigned(self.size)``
+        :class:`.Shape`
+            :py:`unsigned(self.size)`
         """
         return unsigned(self.size)
 
@@ -191,21 +192,21 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
         Returns
         -------
         :class:`View`
-            ``View(self, target)``
+            :py:`View(self, target)`
         """
         return View(self, target)
 
     def const(self, init):
         """Convert a constant initializer to a constant.
 
-        Converts ``init``, which may be a sequence or a mapping of field values, to a constant.
+        Converts :py:`init`, which may be a sequence or a mapping of field values, to a constant.
 
         Returns
         -------
         :class:`Const`
             A constant that has the same value as a view with this layout that was initialized with
             an all-zero value and had every field assigned to the corresponding value in the order
-            in which they appear in ``init``.
+            in which they appear in :py:`init`.
         """
         if init is None:
             iterator = iter(())
@@ -222,18 +223,30 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
             field = self[key]
             cast_field_shape = Shape.cast(field.shape)
             if isinstance(field.shape, ShapeCastable):
-                key_value = Const.cast(field.shape.const(key_value))
+                key_value = hdl.Const.cast(field.shape.const(key_value))
                 if key_value.shape() != cast_field_shape:
                     raise ValueError("Constant returned by {!r}.const() must have the shape that "
                                      "it casts to, {!r}, and not {!r}"
                                      .format(field.shape, cast_field_shape,
                                              key_value.shape()))
-            elif not isinstance(key_value, Const):
-                key_value = Const(key_value, cast_field_shape)
+            elif not isinstance(key_value, hdl.Const):
+                key_value = hdl.Const(key_value, cast_field_shape)
             mask = ((1 << cast_field_shape.width) - 1) << field.offset
             int_value &= ~mask
             int_value |= (key_value.value << field.offset) & mask
-        return View(self, Const(int_value, self.as_shape()))
+        return Const(self, int_value)
+
+    def from_bits(self, raw):
+        """Convert a bit pattern to a constant.
+
+        Converts :py:`raw`, which is an :class:`int`, to a constant.
+
+        Returns
+        -------
+        :class:`Const`
+            :py:`Const(self, raw)`
+        """
+        return Const(self, raw)
 
     def _value_repr(self, value):
         yield Repr(FormatInt(), value)
@@ -279,7 +292,7 @@ class StructLayout(Layout):
 
     Attributes
     ----------
-    members : mapping of :class:`str` to :ref:`shape-like <lang-shapelike>`
+    members : mapping of :class:`str` to :class:`.ShapeLike`
         Dictionary of structure members.
     """
 
@@ -350,7 +363,7 @@ class UnionLayout(Layout):
 
     Attributes
     ----------
-    members : mapping of :class:`str` to :ref:`shape-like <lang-shapelike>`
+    members : mapping of :class:`str` to :class:`.ShapeLike`
         Dictionary of union members.
     """
     def __init__(self, members):
@@ -425,7 +438,7 @@ class ArrayLayout(Layout):
 
     Attributes
     ----------
-    elem_shape : :ref:`shape-like <lang-shapelike>`
+    elem_shape : :class:`.ShapeLike`
         Shape of an individual element.
     length : :class:`int`
         Amount of elements.
@@ -573,7 +586,10 @@ class View(ValueCastable):
     Creating a view
     ###############
 
-    A view must be created using an explicitly provided layout and target. To create a new :class:`Signal` that is wrapped in a :class:`View` with a given ``layout``, use ``Signal(layout, ...)``, which for a :class:`Layout` is equivalent to ``View(layout, Signal(...))``.
+    A view must be created using an explicitly provided layout and target. To create a new
+    :class:`Signal` that is wrapped in a :class:`View` with a given :py:`layout`, use
+    :py:`Signal(layout, ...)`, which for a :class:`Layout` is equivalent to
+    :py:`View(layout, Signal(...))`.
 
     Accessing a view
     ################
@@ -582,46 +598,46 @@ class View(ValueCastable):
     corresponding to the field with that index or name, which is itself either a value or
     a value-castable object. If the shape of the field is a :class:`Layout`, it will be
     a :class:`View`; if it is a class deriving from :class:`Struct` or :class:`Union`, it
-    will be an instance of that data class; if it is another
-    :ref:`shape-like <lang-shapelike>` object implementing ``__call__``, it will be
-    the result of calling that method.
+    will be an instance of that data class; if it is another :ref:`shape-like <lang-shapelike>`
+    object implementing :meth:`~.ShapeCastable.__call__`, it will be the result of calling that
+    method.
 
     Slicing a view whose layout is an :class:`ArrayLayout` can be done with an index that is
-    an Amaranth value instead of a constant integer. The returned element is chosen dynamically
+    an Amaranth value rather than a constant integer. The returned element is chosen dynamically
     in that case.
 
-    A view can only be compared for equality with another view of the same layout,
-    returning a single-bit value. No other operators are supported on views. If required,
-    a view can be converted back to its underlying value via :meth:`as_value`.
+    A view can only be compared for equality with another view or constant with the same layout,
+    returning a single-bit :class:`.Value`. No other operators are supported. A view can be
+    lowered to a :class:`.Value` using :meth:`as_value`.
 
     Custom view classes
     ###################
 
     The :class:`View` class can be inherited from to define additional properties or methods on
-    a view. The only two names that are reserved on instances of :class:`View` are :meth:`as_value`
-    and :meth:`eq`, leaving the rest to the developer. The :class:`Struct` and :class:`Union`
-    classes provided in this module are subclasses of :class:`View` that also provide a concise way
-    to define a layout.
+    a view. The only three names that are reserved on instances of :class:`View` and :class:`Const`
+    are :meth:`as_value`, :meth:`Const.as_bits`, and :meth:`eq`, leaving the rest to the developer.
+    The :class:`Struct` and :class:`Union` classes provided in this module are subclasses of
+    :class:`View` that also provide a concise way to define a layout.
     """
     def __init__(self, layout, target):
         try:
             cast_layout = Layout.cast(layout)
         except TypeError as e:
-            raise TypeError("View layout must be a layout, not {!r}"
+            raise TypeError("Layout of a view must be a Layout, not {!r}"
                             .format(layout)) from e
         try:
             cast_target = Value.cast(target)
         except TypeError as e:
-            raise TypeError("View target must be a value-castable object, not {!r}"
+            raise TypeError("Target of a view must be a value-castable object, not {!r}"
                             .format(target)) from e
         if len(cast_target) != cast_layout.size:
-            raise ValueError("View target is {} bit(s) wide, which is not compatible with "
-                             "the {} bit(s) wide view layout"
+            raise ValueError("Target of a view is {} bit(s) wide, which is not compatible with "
+                             "its {} bit(s) wide layout"
                              .format(len(cast_target), cast_layout.size))
         for name, field in cast_layout:
             if isinstance(name, str) and name[0] != "_" and hasattr(type(self), name):
-                warnings.warn("View layout includes a field {!r} that will be shadowed by "
-                              "the view attribute '{}.{}.{}'"
+                warnings.warn("Layout of a view includes a field {!r} that will be shadowed by "
+                              "the attribute '{}.{}.{}'"
                               .format(name, type(self).__module__, type(self).__qualname__, name),
                               SyntaxWarning, stacklevel=2)
         self.__orig_layout = layout
@@ -634,7 +650,7 @@ class View(ValueCastable):
         Returns
         -------
         :class:`Layout`
-            The ``layout`` provided when constructing the view.
+            The :py:`layout` provided when constructing the view.
         """
         return self.__orig_layout
 
@@ -643,8 +659,8 @@ class View(ValueCastable):
 
         Returns
         -------
-        :class:`Value`
-            The ``target`` provided when constructing the view, or the :class:`Signal` that
+        :class:`.Value`
+            The :py:`target` provided when constructing the view, or the :class:`Signal` that
             was created.
         """
         return self.__target
@@ -654,52 +670,53 @@ class View(ValueCastable):
 
         Returns
         -------
-        :class:`Assign`
-            ``self.as_value().eq(other)``
+        :class:`.Assign`
+            :py:`self.as_value().eq(other)`
         """
         return self.as_value().eq(other)
 
     def __getitem__(self, key):
         """Slice the underlying value.
 
-        A field corresponding to ``key`` is looked up in the layout. If the field's shape is
-        a shape-castable object that has a ``__call__`` method, it is called and the result is
-        returned. Otherwise, ``as_shape`` is called repeatedly on the shape until either an object
-        with a ``__call__`` method is reached, or a ``Shape`` is returned. In the latter case,
-        returns an unspecified Amaranth expression with the right shape.
+        A field corresponding to :py:`key` is looked up in the layout. If the field's shape is
+        a shape-castable object that has a :meth:`~.ShapeCastable.__call__` method, it is called and
+        the result is returned. Otherwise, :meth:`~.ShapeCastable.as_shape` is called repeatedly on
+        the shape until either an object with a :meth:`~.ShapeCastable.__call__` method is reached,
+        or a :class:`.Shape` is returned. In the latter case, returns an unspecified Amaranth
+        expression with the right shape.
 
         Arguments
         ---------
-        key : :class:`str` or :class:`int` or :class:`ValueCastable`
+        key : :class:`str` or :class:`int` or :class:`.ValueCastable`
             Name or index of a field.
 
         Returns
         -------
-        :class:`Value` or :class:`ValueCastable`, inout
+        :class:`.Value` or :class:`.ValueCastable`, :ref:`assignable <lang-assignable>`
             A slice of the underlying value defined by the field.
 
         Raises
         ------
-        KeyError
-            If the layout does not define a field corresponding to ``key``.
-        TypeError
-            If ``key`` is a value-castable object, but the layout of the view is not
-            a :class:`ArrayLayout`.
-        TypeError
-            If ``ShapeCastable.__call__`` does not return a value or a value-castable object.
+        :exc:`KeyError`
+            If the layout does not define a field corresponding to :py:`key`.
+        :exc:`TypeError`
+            If :py:`key` is a value-castable object, but the layout of the view is not
+            an :class:`ArrayLayout`.
+        :exc:`TypeError`
+            If :meth:`.ShapeCastable.__call__` does not return a value or a value-castable object.
         """
         if isinstance(self.__layout, ArrayLayout):
             if not isinstance(key, (int, Value, ValueCastable)):
-                raise TypeError("Views with array layout may only be indexed with an integer "
-                                "or a value, not {!r}"
-                                .format(key))
+                raise TypeError(
+                    f"View with array layout may only be indexed with an integer or a value, "
+                    f"not {key!r}")
             shape = self.__layout.elem_shape
             value = self.__target.word_select(key, Shape.cast(self.__layout.elem_shape).width)
         else:
             if isinstance(key, (Value, ValueCastable)):
-                raise TypeError("Only views with array layout, not {!r}, may be indexed "
-                                "with a value"
-                                .format(self.__layout))
+                raise TypeError(
+                    f"Only views with array layout, not {self.__layout!r}, may be indexed with "
+                    f"a value")
             field = self.__layout[key]
             shape = field.shape
             value = self.__target[field.offset:field.offset + field.width]
@@ -708,9 +725,9 @@ class View(ValueCastable):
         if isinstance(shape, ShapeCastable):
             value = shape(value)
             if not isinstance(value, (Value, ValueCastable)):
-                raise TypeError("{!r}.__call__() must return a value or "
-                                "a value-castable object, not {!r}"
-                                .format(shape, value))
+                raise TypeError(
+                    f"{shape!r}.__call__() must return a value or a value-castable object, not "
+                    f"{value!r}")
             return value
         if Shape.cast(shape).signed:
             return value.as_signed()
@@ -720,40 +737,48 @@ class View(ValueCastable):
     def __getattr__(self, name):
         """Access a field of the underlying value.
 
-        Returns ``self[name]``.
+        Returns :py:`self[name]`.
 
         Raises
         ------
-        AttributeError
-            If the layout does not define a field called ``name``, or if ``name`` starts with
+        :exc:`AttributeError`
+            If the layout does not define a field called :py:`name`, or if :py:`name` starts with
             an underscore.
         """
         if isinstance(self.__layout, ArrayLayout):
-            raise AttributeError("View of {!r} with an array layout does not have fields"
-                                 .format(self.__target))
+            raise AttributeError(
+                f"View with an array layout does not have fields")
         try:
             item = self[name]
         except KeyError:
-            raise AttributeError("View of {!r} does not have a field {!r}; "
-                                 "did you mean one of: {}?"
-                                 .format(self.__target, name,
-                                         ", ".join(repr(name)
-                                                   for name, field in self.__layout)))
+            raise AttributeError(
+                f"View with layout {self.__layout!r} does not have a field {name!r}; did you mean "
+                f"one of: {', '.join(repr(name) for name, field in self.__layout)}?")
         if name.startswith("_"):
-            raise AttributeError("View of {!r} field {!r} has a reserved name and may only be "
-                                 "accessed by indexing"
-                                 .format(self.__target, name))
+            raise AttributeError(
+                f"Field {name!r} of view with layout {self.__layout!r} has a reserved name and "
+                f"may only be accessed by indexing")
         return item
 
     def __eq__(self, other):
-        if not isinstance(other, View) or self.__layout != other.__layout:
-            raise TypeError(f"View of {self.__layout!r} can only be compared to another view of the same layout, not {other!r}")
-        return self.__target == other.__target
+        if isinstance(other, View) and self.__layout == other.__layout:
+            return self.__target == other.__target
+        elif isinstance(other, Const) and self.__layout == other._Const__layout:
+            return self.__target == other.as_value()
+        else:
+            raise TypeError(
+                f"View with layout {self.__layout!r} can only be compared to another view or "
+                f"constant with the same layout, not {other!r}")
 
     def __ne__(self, other):
-        if not isinstance(other, View) or self.__layout != other.__layout:
-            raise TypeError(f"View of {self.__layout!r} can only be compared to another view of the same layout, not {other!r}")
-        return self.__target != other.__target
+        if isinstance(other, View) and self.__layout == other.__layout:
+            return self.__target != other.__target
+        elif isinstance(other, Const) and self.__layout == other._Const__layout:
+            return self.__target != other.as_value()
+        else:
+            raise TypeError(
+                f"View with layout {self.__layout!r} can only be compared to another view or "
+                f"constant with the same layout, not {other!r}")
 
     def __add__(self, other):
         raise TypeError("Cannot perform arithmetic operations on a View")
@@ -789,6 +814,223 @@ class View(ValueCastable):
         return f"{self.__class__.__name__}({self.__layout!r}, {self.__target!r})"
 
 
+class Const(ValueCastable):
+    """A constant value viewed through the lens of a layout.
+
+    The :class:`Const` class is similar to the :class:`View` class, except that its target is
+    a specific bit pattern and operations on it return constants.
+
+    Creating a constant
+    ###################
+
+    A constant can be created from a :class:`dict` or :class:`list` of field values using
+    :meth:`Layout.const`, or from a bit pattern using :meth:`Layout.from_bits`.
+
+    Accessing a constant
+    ####################
+
+    Slicing a constant or accessing its attributes returns a part of the underlying value
+    corresponding to the field with that index or name. If the shape of the field is
+    a :class:`Layout`, the returned value is a :class:`Const`; if it is a different
+    :ref:`shape-like <lang-shapelike>` object implementing :meth:`~.ShapeCastable.from_bits`,
+    it will be the result of calling that method; otherwise, it is an :class:`int`.
+
+    Slicing a constant whose layout is an :class:`ArrayLayout` can be done with an index that is
+    an Amaranth value rather than a constant integer. The returned element is chosen dynamically
+    in that case, and the resulting value will be a :class:`View` instead of a :class:`Const`.
+
+    A :class:`Const` can only be compared for equality with another constant or view that has
+    the same layout. When compared with another constant, the result will be a :class:`bool`.
+    When compared with a view, the result will be a single-bit :class:`.Value`. No other operators
+    are supported. A constant can be lowered to a :class:`.Value` using :meth:`as_value`, or to
+    its underlying bit pattern using :meth:`as_bits`.
+    """
+    def __init__(self, layout, target):
+        try:
+            cast_layout = Layout.cast(layout)
+        except TypeError as e:
+            raise TypeError(f"Layout of a constant must be a Layout, not {layout!r}") from e
+        try:
+            target = operator.index(target)
+        except TypeError as e:
+            raise TypeError(f"Target of a constant must be an int, not {target!r}") from e
+        if target not in range(1 << cast_layout.size):
+            raise ValueError(f"Target of a constant does not fit in {cast_layout.size} bit(s)")
+        for name, field in cast_layout:
+            if isinstance(name, str) and name[0] != "_" and hasattr(type(self), name):
+                warnings.warn("Layout of a constant includes a field {!r} that will be shadowed by "
+                              "the attribute '{}.{}.{}'"
+                              .format(name, type(self).__module__, type(self).__qualname__, name),
+                              SyntaxWarning, stacklevel=2)
+        self.__orig_layout = layout
+        self.__layout = cast_layout
+        self.__target = target
+
+    def shape(self):
+        """Get layout of this constant.
+
+        Returns
+        -------
+        :class:`Layout`
+            The :py:`layout` provided when constructing the constant.
+        """
+        return self.__orig_layout
+
+    def as_bits(self):
+        """Get underlying bit pattern.
+
+        Returns
+        -------
+        :class:`int`
+            The :py:`target` provided when constructing the constant.
+        """
+        return self.__target
+
+    def as_value(self):
+        """Convert to a value.
+
+        Returns
+        -------
+        :class:`.Const`
+            The bit pattern of this constant, as a :class:`.Value`.
+        """
+        return hdl.Const(self.__target, self.__layout.size)
+
+    def __getitem__(self, key):
+        """Slice the underlying value.
+
+        A field corresponding to :py:`key` is looked up in the layout. If the field's shape is
+        a shape-castable object that has a :meth:`~.ShapeCastable.from_bits` method, returns
+        the result of calling that method. Otherwise, returns an :class:`int`.
+
+        Arguments
+        ---------
+        key : :class:`str` or :class:`int` or :class:`.ValueCastable`
+            Name or index of a field.
+
+        Returns
+        -------
+        unspecified type or :class:`int`
+            A slice of the underlying value defined by the field.
+
+        Raises
+        ------
+        :exc:`KeyError`
+            If the layout does not define a field corresponding to :py:`key`.
+        :exc:`TypeError`
+            If :py:`key` is a value-castable object, but the layout of the constant is not
+            an :class:`ArrayLayout`.
+        :exc:`Exception`
+            If the bit pattern of the field is not valid according to
+            :meth:`.ShapeCastable.from_bits`. Usually this will be a :exc:`ValueError`.
+        """
+        if isinstance(self.__layout, ArrayLayout):
+            if isinstance(key, (Value, ValueCastable)):
+                return View(self.__layout, self.as_value())[key]
+            if not isinstance(key, int):
+                raise TypeError(
+                    f"Constant with array layout may only be indexed with an integer or a value, "
+                    f"not {key!r}")
+            shape = self.__layout.elem_shape
+            elem_width = Shape.cast(self.__layout.elem_shape).width
+            value = (self.__target >> key * elem_width) & ((1 << elem_width) - 1)
+        else:
+            if isinstance(key, (Value, ValueCastable)):
+                raise TypeError(
+                    f"Only constants with array layout, not {self.__layout!r}, may be indexed with "
+                    f"a value")
+            field = self.__layout[key]
+            shape = field.shape
+            value = (self.__target >> field.offset) & ((1 << field.width) - 1)
+        # Field guarantees that the shape-castable object is well-formed, so there is no need
+        # to handle erroneous cases here.
+        if isinstance(shape, ShapeCastable):
+            return shape.from_bits(value)
+        return hdl.Const(value, Shape.cast(shape)).value
+
+    def __getattr__(self, name):
+        """Access a field of the underlying value.
+
+        Returns :py:`self[name]`.
+
+        Raises
+        ------
+        :exc:`AttributeError`
+            If the layout does not define a field called :py:`name`, or if :py:`name` starts with
+            an underscore.
+        :exc:`Exception`
+            If the bit pattern of the field is not valid according to
+            :meth:`.ShapeCastable.from_bits`. Usually this will be a :exc:`ValueError`.
+        """
+        if isinstance(self.__layout, ArrayLayout):
+            raise AttributeError(
+                f"Constant with an array layout does not have fields")
+        try:
+            item = self[name]
+        except KeyError:
+            raise AttributeError(
+                f"Constant with layout {self.__layout!r} does not have a field {name!r}; did you mean "
+                f"one of: {', '.join(repr(name) for name, field in self.__layout)}?")
+        if name.startswith("_"):
+            raise AttributeError(
+                f"Field {name!r} of constant with layout {self.__layout!r} has a reserved name and "
+                f"may only be accessed by indexing")
+        return item
+
+    def __eq__(self, other):
+        if isinstance(other, View) and self.__layout == other._View__layout:
+            return self.as_value() == other._View__target
+        elif isinstance(other, Const) and self.__layout == other.__layout:
+            return self.__target == other.__target
+        else:
+            raise TypeError(
+                f"Constant with layout {self.__layout!r} can only be compared to another view or "
+                f"constant with the same layout, not {other!r}")
+
+    def __ne__(self, other):
+        if isinstance(other, View) and self.__layout == other._View__layout:
+            return self.as_value() != other._View__target
+        elif isinstance(other, Const) and self.__layout == other.__layout:
+            return self.__target != other.__target
+        else:
+            raise TypeError(
+                f"Constant with layout {self.__layout!r} can only be compared to another view or "
+                f"constant with the same layout, not {other!r}")
+
+    def __add__(self, other):
+        raise TypeError("Cannot perform arithmetic operations on a lib.data.Const")
+
+    __radd__ = __add__
+    __sub__ = __add__
+    __rsub__ = __add__
+    __mul__ = __add__
+    __rmul__ = __add__
+    __floordiv__ = __add__
+    __rfloordiv__ = __add__
+    __mod__ = __add__
+    __rmod__ = __add__
+    __lshift__ = __add__
+    __rlshift__ = __add__
+    __rshift__ = __add__
+    __rrshift__ = __add__
+    __lt__ = __add__
+    __le__ = __add__
+    __gt__ = __add__
+    __ge__ = __add__
+
+    def __and__(self, other):
+        raise TypeError("Cannot perform bitwise operations on a lib.data.Const")
+
+    __rand__ = __and__
+    __or__ = __and__
+    __ror__ = __and__
+    __xor__ = __and__
+    __rxor__ = __and__
+
+    def __repr__(self):
+        return f"{self.__class__.__name__}({self.__layout!r}, {self.__target!r})"
+
+
 class _AggregateMeta(ShapeCastable, type):
     def __new__(metacls, name, bases, namespace):
         if "__annotations__" not in namespace:
@@ -847,6 +1089,9 @@ class _AggregateMeta(ShapeCastable, type):
             fields.update(init or {})
             return cls.as_shape().const(fields)
 
+    def from_bits(cls, bits):
+        return cls.as_shape().from_bits(bits)
+
     def _value_repr(cls, value):
         return cls.__layout._value_repr(value)
 
@@ -881,7 +1126,7 @@ class Struct(View, metaclass=_AggregateMeta):
             def is_subnormal(self):
                 return self.exponent == 0
 
-    The ``IEEE754Single`` class itself can be used where a :ref:`shape <lang-shapes>` is expected:
+    The :py:`IEEE754Single` class itself can be used where a :ref:`shape <lang-shapes>` is expected:
 
     .. doctest::
 
diff --git a/amaranth/lib/enum.py b/amaranth/lib/enum.py
index 135bb58..adf94d6 100644
--- a/amaranth/lib/enum.py
+++ b/amaranth/lib/enum.py
@@ -173,6 +173,9 @@ class EnumMeta(ShapeCastable, py_enum.EnumMeta):
             member = cls(init)
         return cls(Const(member.value, cls.as_shape()))
 
+    def from_bits(cls, bits):
+        return cls(bits)
+
     def _value_repr(cls, value):
         yield Repr(FormatEnum(cls), value)
 
diff --git a/docs/changes.rst b/docs/changes.rst
index cca07b0..1ba3761 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -49,6 +49,7 @@ Implemented RFCs
 .. _RFC 45: https://amaranth-lang.org/rfcs/0045-lib-memory.html
 .. _RFC 46: https://amaranth-lang.org/rfcs/0046-shape-range-1.html
 .. _RFC 50: https://amaranth-lang.org/rfcs/0050-print.html
+.. _RFC 51: https://amaranth-lang.org/rfcs/0051-const-from-bits.html
 .. _RFC 53: https://amaranth-lang.org/rfcs/0053-ioport.html
 .. _RFC 55: https://amaranth-lang.org/rfcs/0055-lib-io.html
 
@@ -59,6 +60,7 @@ Implemented RFCs
 * `RFC 45`_: Move ``hdl.Memory`` to ``lib.Memory``
 * `RFC 46`_: Change ``Shape.cast(range(1))`` to ``unsigned(0)``
 * `RFC 50`_: ``Print`` statement and string formatting
+* `RFC 51`_: Add ``ShapeCastable.from_bits`` and ``amaranth.lib.data.Const``
 * `RFC 53`_: Low-level I/O primitives
 
 
@@ -70,6 +72,7 @@ Language changes
 * Added: :class:`Slice` objects have been made const-castable.
 * Added: :func:`amaranth.utils.ceil_log2`, :func:`amaranth.utils.exact_log2`. (`RFC 17`_)
 * Added: :class:`Format` objects, :class:`Print` statements, messages in :class:`Assert`, :class:`Assume` and :class:`Cover`. (`RFC 50`_)
+* Added: :meth:`ShapeCastable.from_bits` method. (`RFC 51`_)
 * Added: IO values, :class:`IOPort` objects, :class:`IOBufferInstance` objects. (`RFC 53`_)
 * 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`_)
@@ -94,6 +97,8 @@ Standard library changes
 .. currentmodule:: amaranth.lib
 
 * Added: :mod:`amaranth.lib.memory`. (`RFC 45`_)
+* Added: :class:`amaranth.lib.data.Const` class. (`RFC 51`_)
+* Changed: :meth:`amaranth.lib.data.Layout.const` returns a :class:`amaranth.lib.data.Const`, not a view (`RFC 51`_)
 * Added: :class:`amaranth.lib.io.SingleEndedPort`, :class:`amaranth.lib.io.DifferentialPort`. (`RFC 55`_)
 * Removed: (deprecated in 0.4) :mod:`amaranth.lib.scheduler`. (`RFC 19`_)
 * Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.FIFOInterface` with ``fwft=False``. (`RFC 20`_)
diff --git a/docs/stdlib/data.rst b/docs/stdlib/data.rst
index 0161790..198e8b1 100644
--- a/docs/stdlib/data.rst
+++ b/docs/stdlib/data.rst
@@ -221,7 +221,7 @@ Modeling structured data
 ========================
 
 .. autoclass:: Field
-.. autoclass:: Layout
+.. autoclass:: Layout()
 
 
 Common data layouts
@@ -237,6 +237,7 @@ Data views
 ==========
 
 .. autoclass:: View
+.. autoclass:: Const
 
 
 Data classes


Test patch:
diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py
index 47d89cc..d477855 100644
--- a/tests/test_hdl_ast.py
+++ b/tests/test_hdl_ast.py
@@ -178,6 +178,9 @@ class MockShapeCastable(ShapeCastable):
     def const(self, init):
         return Const(init, self.dest)
 
+    def from_bits(self, bits):
+        return bits
+
 
 class ShapeCastableTestCase(FHDLTestCase):
     def test_no_override(self):
@@ -208,6 +211,25 @@ class ShapeCastableTestCase(FHDLTestCase):
                 r"^Can't instantiate abstract class ShapeCastable$"):
             ShapeCastable()
 
+    def test_no_from_bits(self):
+        with self.assertWarnsRegex(DeprecationWarning,
+                r"^Class 'MockShapeCastableNoFromBits' deriving from 'ShapeCastable' does "
+                r"not override the 'from_bits' method, which will be required in Amaranth 0.6$"):
+            class MockShapeCastableNoFromBits(ShapeCastable):
+                def __init__(self, dest):
+                    self.dest = dest
+
+                def as_shape(self):
+                    return self.dest
+
+                def __call__(self, value):
+                    return value
+
+                def const(self, init):
+                    return Const(init, self.dest)
+
+        self.assertEqual(MockShapeCastableNoFromBits(unsigned(2)).from_bits(123), 123)
+
 
 class ShapeLikeTestCase(FHDLTestCase):
     def test_construct(self):
@@ -514,6 +536,9 @@ class ConstTestCase(FHDLTestCase):
             def const(self, init):
                 return MockConstValue(init)
 
+            def from_bits(self, bits):
+                return bits
+
         s = Const(10, MockConstShape())
         self.assertIsInstance(s, MockConstValue)
         self.assertEqual(s.value, 10)
@@ -1186,6 +1211,9 @@ class SignalTestCase(FHDLTestCase):
             def const(self, init):
                 return int(init, 16)
 
+            def from_bits(self, bits):
+                return bits
+
         s1 = Signal(CastableFromHex(), init="aa")
         self.assertEqual(s1.init, 0xaa)
 
diff --git a/tests/test_lib_data.py b/tests/test_lib_data.py
index 071692e..19c46db 100644
--- a/tests/test_lib_data.py
+++ b/tests/test_lib_data.py
@@ -22,6 +22,9 @@ class MockShapeCastable(ShapeCastable):
     def const(self, init):
         return Const(init, self.shape)
 
+    def from_bits(self, bits):
+        return bits
+
 
 class FieldTestCase(TestCase):
     def test_construct(self):
@@ -417,6 +420,9 @@ class LayoutTestCase(FHDLTestCase):
             def const(self, init):
                 return int(init, 16)
 
+            def from_bits(self, bits):
+                return bits
+
         sl = data.StructLayout({"f": CastableFromHex()})
         self.assertRepr(sl.const({"f": "aa"}).as_value(), "(const 8'd170)")
 
@@ -467,13 +473,13 @@ class ViewTestCase(FHDLTestCase):
 
     def test_layout_wrong(self):
         with self.assertRaisesRegex(TypeError,
-                r"^View layout must be a layout, not <.+?>$"):
+                r"^Layout of a view must be a Layout, not <.+?>$"):
             data.View(object(), Signal(1))
 
     def test_layout_conflict_with_attr(self):
         with self.assertWarnsRegex(SyntaxWarning,
-                r"^View layout includes a field 'as_value' that will be shadowed by the view "
-                r"attribute 'amaranth\.lib\.data\.View\.as_value'$"):
+                r"^Layout of a view includes a field 'as_value' that will be shadowed by "
+                r"the attribute 'amaranth\.lib\.data\.View\.as_value'$"):
             data.View(data.StructLayout({"as_value": unsigned(1)}), Signal(1))
 
     def test_layout_conflict_with_attr_derived(self):
@@ -481,20 +487,20 @@ class ViewTestCase(FHDLTestCase):
             def foo(self):
                 pass
         with self.assertWarnsRegex(SyntaxWarning,
-                r"^View layout includes a field 'foo' that will be shadowed by the view "
-                r"attribute 'tests\.test_lib_data\.ViewTestCase\."
+                r"^Layout of a view includes a field 'foo' that will be shadowed by "
+                r"the attribute 'tests\.test_lib_data\.ViewTestCase\."
                 r"test_layout_conflict_with_attr_derived\.<locals>.DerivedView\.foo'$"):
             DerivedView(data.StructLayout({"foo": unsigned(1)}), Signal(1))
 
     def test_target_wrong_type(self):
         with self.assertRaisesRegex(TypeError,
-                r"^View target must be a value-castable object, not <.+?>$"):
+                r"^Target of a view must be a value-castable object, not <.+?>$"):
             data.View(data.StructLayout({}), object())
 
     def test_target_wrong_size(self):
         with self.assertRaisesRegex(ValueError,
-                r"^View target is 2 bit\(s\) wide, which is not compatible with the 1 bit\(s\) "
-                r"wide view layout$"):
+                r"^Target of a view is 2 bit\(s\) wide, which is not compatible with its 1 bit\(s\) "
+                r"wide layout$"):
             data.View(data.StructLayout({"a": unsigned(1)}), Signal(2))
 
     def test_getitem(self):
@@ -540,6 +546,9 @@ class ViewTestCase(FHDLTestCase):
             def const(self, init):
                 return Const(init, 2)
 
+            def from_bits(self, bits):
+                return bits
+
         v = Signal(data.StructLayout({
             "f": Reverser()
         }))
@@ -557,6 +566,9 @@ class ViewTestCase(FHDLTestCase):
             def const(self, init):
                 return Const(init, 2)
 
+            def from_bits(self, bits):
+                return bits
+
         v = Signal(data.StructLayout({
             "f": WrongCastable()
         }))
@@ -606,14 +618,13 @@ class ViewTestCase(FHDLTestCase):
 
     def test_attr_wrong_missing(self):
         with self.assertRaisesRegex(AttributeError,
-                r"^View of \(sig \$signal\) does not have a field 'a'; "
-                r"did you mean one of: 'b', 'c'\?$"):
+                r"^View with layout .* does not have a field 'a'; did you mean one of: 'b', 'c'\?$"):
             Signal(data.StructLayout({"b": unsigned(1), "c": signed(1)})).a
 
     def test_attr_wrong_reserved(self):
         with self.assertRaisesRegex(AttributeError,
-                r"^View of \(sig \$signal\) field '_c' has a reserved name "
-                r"and may only be accessed by indexing$"):
+                r"^Field '_c' of view with layout .* has a reserved name and may only be accessed "
+                r"by indexing$"):
             Signal(data.StructLayout({"_c": signed(1)}))._c
 
     def test_signal_like(self):
@@ -623,13 +634,13 @@ class ViewTestCase(FHDLTestCase):
 
     def test_bug_837_array_layout_getitem_str(self):
         with self.assertRaisesRegex(TypeError,
-                r"^Views with array layout may only be indexed with an integer or a value, "
+                r"^View with array layout may only be indexed with an integer or a value, "
                 r"not 'init'$"):
             Signal(data.ArrayLayout(unsigned(1), 1), init=[0])["init"]
 
     def test_bug_837_array_layout_getattr(self):
         with self.assertRaisesRegex(AttributeError,
-                r"^View of \(sig \$signal\) with an array layout does not have fields$"):
+                r"^View with an array layout does not have fields$"):
             Signal(data.ArrayLayout(unsigned(1), 1), init=[0]).init
 
     def test_eq(self):
@@ -639,16 +650,20 @@ class ViewTestCase(FHDLTestCase):
         self.assertRepr(s1 == s2, "(== (sig s1) (sig s2))")
         self.assertRepr(s1 != s2, "(!= (sig s1) (sig s2))")
         with self.assertRaisesRegex(TypeError,
-                r"^View of .* can only be compared to another view of the same layout, not .*$"):
+                r"^View with layout .* can only be compared to another view or constant "
+                r"with the same layout, not .*$"):
             s1 == s3
         with self.assertRaisesRegex(TypeError,
-                r"^View of .* can only be compared to another view of the same layout, not .*$"):
+                r"^View with layout .* can only be compared to another view or constant "
+                r"with the same layout, not .*$"):
             s1 != s3
         with self.assertRaisesRegex(TypeError,
-                r"^View of .* can only be compared to another view of the same layout, not .*$"):
+                r"^View with layout .* can only be compared to another view or constant "
+                r"with the same layout, not .*$"):
             s1 == Const(0, 2)
         with self.assertRaisesRegex(TypeError,
-                r"^View of .* can only be compared to another view of the same layout, not .*$"):
+                r"^View with layout .* can only be compared to another view or constant "
+                r"with the same layout, not .*$"):
             s1 != Const(0, 2)
 
     def test_operator(self):
@@ -690,6 +705,251 @@ class ViewTestCase(FHDLTestCase):
         self.assertRepr(s1, "View(StructLayout({'a': unsigned(2)}), (sig s1))")
 
 
+class ConstTestCase(FHDLTestCase):
+    def test_construct(self):
+        c = data.Const(data.StructLayout({"a": unsigned(1), "b": unsigned(2)}), 5)
+        self.assertRepr(Value.cast(c), "(const 3'd5)")
+        self.assertEqual(c.shape(), data.StructLayout({"a": unsigned(1), "b": unsigned(2)}))
+        self.assertEqual(c.as_bits(), 5)
+        self.assertEqual(c["a"], 1)
+        self.assertEqual(c["b"], 2)
+
+    def test_construct_const(self):
+        c = Const({"a": 1, "b": 2}, data.StructLayout({"a": unsigned(1), "b": unsigned(2)}))
+        self.assertRepr(Const.cast(c), "(const 3'd5)")
+        self.assertEqual(c.a, 1)
+        self.assertEqual(c.b, 2)
+
+    def test_layout_wrong(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Layout of a constant must be a Layout, not <.+?>$"):
+            data.Const(object(), 1)
+
+    def test_layout_conflict_with_attr(self):
+        with self.assertWarnsRegex(SyntaxWarning,
+                r"^Layout of a constant includes a field 'as_value' that will be shadowed by "
+                r"the attribute 'amaranth\.lib\.data\.Const\.as_value'$"):
+            data.Const(data.StructLayout({"as_value": unsigned(1)}), 1)
+
+    def test_layout_conflict_with_attr_derived(self):
+        class DerivedConst(data.Const):
+            def foo(self):
+                pass
+        with self.assertWarnsRegex(SyntaxWarning,
+                r"^Layout of a constant includes a field 'foo' that will be shadowed by "
+                r"the attribute 'tests\.test_lib_data\.ConstTestCase\."
+                r"test_layout_conflict_with_attr_derived\.<locals>.DerivedConst\.foo'$"):
+            DerivedConst(data.StructLayout({"foo": unsigned(1)}), 1)
+
+    def test_target_wrong_type(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Target of a constant must be an int, not <.+?>$"):
+            data.Const(data.StructLayout({}), object())
+
+    def test_target_wrong_value(self):
+        with self.assertRaisesRegex(ValueError,
+                r"^Target of a constant does not fit in 1 bit\(s\)$"):
+            data.Const(data.StructLayout({"a": unsigned(1)}), 2)
+
+    def test_getitem(self):
+        l = data.StructLayout({
+            "u": unsigned(1),
+            "v": unsigned(1)
+        })
+        v = data.Const(data.StructLayout({
+            "a": unsigned(2),
+            "s": data.StructLayout({
+                "b": unsigned(1),
+                "c": unsigned(3)
+            }),
+            "p": 1,
+            "q": signed(1),
+            "r": data.ArrayLayout(unsigned(2), 2),
+            "t": data.ArrayLayout(data.StructLayout({
+                "u": unsigned(1),
+                "v": unsigned(1)
+            }), 2),
+        }), 0xabcd)
+        cv = Value.cast(v)
+        i = Signal(1)
+        self.assertEqual(cv.shape(), unsigned(16))
+        self.assertEqual(v["a"], 1)
+        self.assertEqual(v["s"]["b"], 1)
+        self.assertEqual(v["s"]["c"], 1)
+        self.assertEqual(v["p"], 1)
+        self.assertEqual(v["q"], -1)
+        self.assertEqual(v["r"][0], 3)
+        self.assertEqual(v["r"][1], 2)
+        self.assertRepr(v["r"][i], "(part (const 4'd11) (sig i) 2 2)")
+        self.assertEqual(v["t"][0], data.Const(l, 2))
+        self.assertEqual(v["t"][1], data.Const(l, 2))
+        self.assertEqual(v["t"][0]["u"], 0)
+        self.assertEqual(v["t"][1]["v"], 1)
+
+    def test_getitem_custom_call(self):
+        class Reverser(ShapeCastable):
+            def as_shape(self):
+                return unsigned(2)
+
+            def __call__(self, value):
+                raise NotImplementedError
+
+            def const(self, init):
+                raise NotImplementedError
+
+            def from_bits(self, bits):
+                return float(bits) / 2
+
+        v = data.Const(data.StructLayout({
+            "f": Reverser()
+        }), 3)
+        self.assertEqual(v.f, 1.5)
+
+    def test_index_wrong_missing(self):
+        with self.assertRaisesRegex(KeyError,
+                r"^'a'$"):
+            data.Const(data.StructLayout({}), 0)["a"]
+
+    def test_index_wrong_struct_dynamic(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Only constants with array layout, not StructLayout\(\{\}\), may be indexed "
+                r"with a value$"):
+            data.Const(data.StructLayout({}), 0)[Signal(1)]
+
+    def test_getattr(self):
+        v = data.Const(data.UnionLayout({
+            "a": unsigned(2),
+            "s": data.StructLayout({
+                "b": unsigned(1),
+                "c": unsigned(3)
+            }),
+            "p": 1,
+            "q": signed(1),
+        }), 13)
+        cv = Const.cast(v)
+        i = Signal(1)
+        self.assertEqual(cv.shape(), unsigned(4))
+        self.assertEqual(v.a, 1)
+        self.assertEqual(v.s.b, 1)
+        self.assertEqual(v.s.c, 6)
+        self.assertEqual(v.p, 1)
+        self.assertEqual(v.q, -1)
+
+    def test_getattr_reserved(self):
+        v = data.Const(data.UnionLayout({
+            "_a": unsigned(2)
+        }), 2)
+        self.assertEqual(v["_a"], 2)
+
+    def test_attr_wrong_missing(self):
+        with self.assertRaisesRegex(AttributeError,
+                r"^Constant with layout .* does not have a field 'a'; did you mean one of: "
+                r"'b', 'c'\?$"):
+            data.Const(data.StructLayout({"b": unsigned(1), "c": signed(1)}), 0).a
+
+    def test_attr_wrong_reserved(self):
+        with self.assertRaisesRegex(AttributeError,
+                r"^Field '_c' of constant with layout .* has a reserved name and may only be "
+                r"accessed by indexing$"):
+            data.Const(data.StructLayout({"_c": signed(1)}), 0)._c
+
+    def test_bug_837_array_layout_getitem_str(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with array layout may only be indexed with an integer or a value, "
+                r"not 'init'$"):
+            data.Const(data.ArrayLayout(unsigned(1), 1), 0)["init"]
+
+    def test_bug_837_array_layout_getattr(self):
+        with self.assertRaisesRegex(AttributeError,
+                r"^Constant with an array layout does not have fields$"):
+            data.Const(data.ArrayLayout(unsigned(1), 1), 0).init
+
+    def test_eq(self):
+        c1 = data.Const(data.StructLayout({"a": unsigned(2)}), 1)
+        c2 = data.Const(data.StructLayout({"a": unsigned(2)}), 1)
+        c3 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
+        c4 = data.Const(data.StructLayout({"a": unsigned(1), "b": unsigned(1)}), 2)
+        s1 = Signal(data.StructLayout({"a": unsigned(2)}))
+        self.assertTrue(c1 == c2)
+        self.assertFalse(c1 != c2)
+        self.assertFalse(c1 == c3)
+        self.assertTrue(c1 != c3)
+        self.assertRepr(c1 == s1, "(== (const 2'd1) (sig s1))")
+        self.assertRepr(c1 != s1, "(!= (const 2'd1) (sig s1))")
+        self.assertRepr(s1 == c1, "(== (sig s1) (const 2'd1))")
+        self.assertRepr(s1 != c1, "(!= (sig s1) (const 2'd1))")
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view or constant with "
+                r"the same layout, not .*$"):
+            c1 == c4
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view or constant with "
+                r"the same layout, not .*$"):
+            c1 != c4
+        with self.assertRaisesRegex(TypeError,
+                r"^View with layout .* can only be compared to another view or constant with "
+                r"the same layout, not .*$"):
+            s1 == c4
+        with self.assertRaisesRegex(TypeError,
+                r"^View with layout .* can only be compared to another view or constant with "
+                r"the same layout, not .*$"):
+            s1 != c4
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view or constant with "
+                r"the same layout, not .*$"):
+            c4 == s1
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view or constant with "
+                r"the same layout, not .*$"):
+            c4 != s1
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view or constant with "
+                r"the same layout, not .*$"):
+            c1 == Const(0, 2)
+        with self.assertRaisesRegex(TypeError,
+                r"^Constant with layout .* can only be compared to another view or constant with "
+                r"the same layout, not .*$"):
+            c1 != Const(0, 2)
+
+    def test_operator(self):
+        s1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
+        s2 = Signal(unsigned(2))
+        for op in [
+            operator.__add__,
+            operator.__sub__,
+            operator.__mul__,
+            operator.__floordiv__,
+            operator.__mod__,
+            operator.__lshift__,
+            operator.__rshift__,
+            operator.__lt__,
+            operator.__le__,
+            operator.__gt__,
+            operator.__ge__,
+        ]:
+            with self.assertRaisesRegex(TypeError,
+                    r"^Cannot perform arithmetic operations on a lib.data.Const$"):
+                op(s1, s2)
+            with self.assertRaisesRegex(TypeError,
+                    r"^Cannot perform arithmetic operations on a lib.data.Const$"):
+                op(s2, s1)
+        for op in [
+            operator.__and__,
+            operator.__or__,
+            operator.__xor__,
+        ]:
+            with self.assertRaisesRegex(TypeError,
+                    r"^Cannot perform bitwise operations on a lib.data.Const$"):
+                op(s1, s2)
+            with self.assertRaisesRegex(TypeError,
+                    r"^Cannot perform bitwise operations on a lib.data.Const$"):
+                op(s2, s1)
+
+    def test_repr(self):
+        s1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
+        self.assertRepr(s1, "Const(StructLayout({'a': unsigned(2)}), 2)")
+
+
 class StructTestCase(FHDLTestCase):
     def test_construct(self):
         class S(data.Struct):
@@ -815,6 +1075,13 @@ class StructTestCase(FHDLTestCase):
         s2 = Signal.like(s1)
         self.assertEqual(s2.shape(), S)
 
+    def test_from_bits(self):
+        class S(data.Struct):
+            a: 1
+        c = S.from_bits(1)
+        self.assertIsInstance(c, data.Const)
+        self.assertEqual(c.a, 1)
+
 
 class UnionTestCase(FHDLTestCase):
     def test_construct(self):
diff --git a/tests/test_lib_enum.py b/tests/test_lib_enum.py
index f4fad0d..6d7799b 100644
--- a/tests/test_lib_enum.py
+++ b/tests/test_lib_enum.py
@@ -110,6 +110,15 @@ class EnumTestCase(FHDLTestCase):
         self.assertRepr(EnumA.const(10), "EnumView(EnumA, (const 8'd10))")
         self.assertRepr(EnumA.const(EnumA.A), "EnumView(EnumA, (const 8'd10))")
 
+    def test_from_bits(self):
+        class EnumA(Enum, shape=2):
+            A = 0
+            B = 1
+            C = 2
+        self.assertIs(EnumA.from_bits(2), EnumA.C)
+        with self.assertRaises(ValueError):
+            EnumA.from_bits(3)
+
     def test_shape_implicit_wrong_in_concat(self):
         class EnumA(Enum):
             A = 0