Implement RFC 30: Component metadata.
Co-authored-by: Catherine <whitequark@whitequark.org>
This commit is contained in:
parent
1d2b9c309e
commit
496432edaa
32
.github/workflows/main.yaml
vendored
32
.github/workflows/main.yaml
vendored
|
@ -126,6 +126,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: docs
|
name: docs
|
||||||
path: docs/_build
|
path: docs/_build
|
||||||
|
- name: Extract schemas
|
||||||
|
run: |
|
||||||
|
pdm run extract-schemas
|
||||||
|
- name: Upload schema archive
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: schema
|
||||||
|
path: schema
|
||||||
|
|
||||||
check-links:
|
check-links:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -154,6 +162,30 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- run: ${{ contains(needs.*.result, 'failure') && 'false' || 'true' }}
|
- run: ${{ contains(needs.*.result, 'failure') && 'false' || 'true' }}
|
||||||
|
|
||||||
|
publish-schemas:
|
||||||
|
needs: document
|
||||||
|
if: ${{ github.repository == 'amaranth-lang/amaranth' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out source code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Download schema archive
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: schema
|
||||||
|
path: schema/
|
||||||
|
- name: Publish development schemas
|
||||||
|
if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' }}
|
||||||
|
uses: JamesIves/github-pages-deploy-action@releases/v4
|
||||||
|
with:
|
||||||
|
repository-name: amaranth-lang/amaranth-lang.github.io
|
||||||
|
ssh-key: ${{ secrets.PAGES_DEPLOY_KEY }}
|
||||||
|
branch: main
|
||||||
|
folder: schema/
|
||||||
|
target-folder: schema/amaranth/
|
||||||
|
|
||||||
publish-docs:
|
publish-docs:
|
||||||
needs: document
|
needs: document
|
||||||
if: ${{ github.repository == 'amaranth-lang/amaranth' }}
|
if: ${{ github.repository == 'amaranth-lang/amaranth' }}
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -9,6 +9,9 @@ __pycache__/
|
||||||
/.venv
|
/.venv
|
||||||
/pdm.lock
|
/pdm.lock
|
||||||
|
|
||||||
|
# metadata schemas
|
||||||
|
/schema
|
||||||
|
|
||||||
# coverage
|
# coverage
|
||||||
/.coverage
|
/.coverage
|
||||||
/htmlcov
|
/htmlcov
|
||||||
|
|
146
amaranth/lib/meta.py
Normal file
146
amaranth/lib/meta.py
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
import jschon
|
||||||
|
import pprint
|
||||||
|
import warnings
|
||||||
|
from abc import abstractmethod, ABCMeta
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["InvalidSchema", "InvalidAnnotation", "Annotation"]
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSchema(Exception):
|
||||||
|
"""Exception raised when a subclass of :class:`Annotation` is defined with a non-conformant
|
||||||
|
:data:`~Annotation.schema`."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAnnotation(Exception):
|
||||||
|
"""Exception raised by :meth:`Annotation.validate` when the JSON representation of
|
||||||
|
an annotation does not conform to its schema."""
|
||||||
|
|
||||||
|
|
||||||
|
class Annotation(metaclass=ABCMeta):
|
||||||
|
"""Interface annotation.
|
||||||
|
|
||||||
|
Annotations are containers for metadata that can be retrieved from an interface object using
|
||||||
|
the :meth:`Signature.annotations <.wiring.Signature.annotations>` method.
|
||||||
|
|
||||||
|
Annotations have a JSON representation whose structure is defined by the `JSON Schema`_
|
||||||
|
language.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: :class:`dict`: Schema of this annotation, expressed in the `JSON Schema`_ language.
|
||||||
|
#:
|
||||||
|
#: Subclasses of :class:`Annotation` must define this class attribute.
|
||||||
|
schema = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __jschon_schema(cls):
|
||||||
|
catalog = jschon.create_catalog("2020-12")
|
||||||
|
return jschon.JSONSchema(cls.schema, catalog=catalog)
|
||||||
|
|
||||||
|
def __init_subclass__(cls, **kwargs):
|
||||||
|
"""
|
||||||
|
Defining a subclass of :class:`Annotation` causes its :data:`schema` to be validated.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
:exc:`InvalidSchema`
|
||||||
|
If :data:`schema` doesn't conform to the `2020-12` draft of `JSON Schema`_.
|
||||||
|
:exc:`InvalidSchema`
|
||||||
|
If :data:`schema` doesn't have a `"$id" keyword`_ at its root. This requirement is
|
||||||
|
specific to :class:`Annotation` schemas.
|
||||||
|
"""
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
|
||||||
|
if not isinstance(cls.schema, dict):
|
||||||
|
raise TypeError(f"Annotation schema must be a dict, not {cls.schema!r}")
|
||||||
|
|
||||||
|
if "$id" not in cls.schema:
|
||||||
|
raise InvalidSchema(f"'$id' keyword is missing from Annotation schema: {cls.schema}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# TODO: Remove this. Ignore a deprecation warning from jschon's rfc3986 dependency.
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||||
|
result = cls.__jschon_schema().validate()
|
||||||
|
except jschon.JSONSchemaError as e:
|
||||||
|
raise InvalidSchema(e) from e
|
||||||
|
|
||||||
|
if not result.valid:
|
||||||
|
raise InvalidSchema("Invalid Annotation schema:\n" +
|
||||||
|
pprint.pformat(result.output("basic")["errors"],
|
||||||
|
sort_dicts=False))
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def origin(self):
|
||||||
|
"""Python object described by this :class:`Annotation` instance.
|
||||||
|
|
||||||
|
Subclasses of :class:`Annotation` must implement this property.
|
||||||
|
"""
|
||||||
|
pass # :nocov:
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def as_json(self):
|
||||||
|
"""Convert to a JSON representation.
|
||||||
|
|
||||||
|
Subclasses of :class:`Annotation` must implement this method.
|
||||||
|
|
||||||
|
JSON representation returned by this method must adhere to :data:`schema` and pass
|
||||||
|
validation by :meth:`validate`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`dict`
|
||||||
|
JSON representation of this annotation, expressed in Python primitive types
|
||||||
|
(:class:`dict`, :class:`list`, :class:`str`, :class:`int`, :class:`bool`).
|
||||||
|
"""
|
||||||
|
pass # :nocov:
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, instance):
|
||||||
|
"""Validate a JSON representation against :attr:`schema`.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
---------
|
||||||
|
instance : :class:`dict`
|
||||||
|
JSON representation to validate, either previously returned by :meth:`as_json`
|
||||||
|
or retrieved from an external source.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
:exc:`InvalidAnnotation`
|
||||||
|
If :py:`instance` doesn't conform to :attr:`schema`.
|
||||||
|
"""
|
||||||
|
# TODO: Remove this. Ignore a deprecation warning from jschon's rfc3986 dependency.
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||||
|
result = cls.__jschon_schema().evaluate(jschon.JSON(instance))
|
||||||
|
|
||||||
|
if not result.valid:
|
||||||
|
raise InvalidAnnotation("Invalid instance:\n" +
|
||||||
|
pprint.pformat(result.output("basic")["errors"],
|
||||||
|
sort_dicts=False))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{type(self).__module__}.{type(self).__qualname__} for {self.origin!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
# For internal use only; we may consider exporting this function in the future.
|
||||||
|
def _extract_schemas(package, *, base_uri, path="schema/"):
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
from importlib.metadata import distribution
|
||||||
|
|
||||||
|
entry_points = distribution(package).entry_points
|
||||||
|
for entry_point in entry_points.select(group="amaranth.lib.meta"):
|
||||||
|
schema = entry_point.load().schema
|
||||||
|
relative_path = entry_point.name # v0.5/component.json
|
||||||
|
schema_filename = pathlib.Path(path) / relative_path
|
||||||
|
assert schema["$id"] == f"{base_uri}/{relative_path}", \
|
||||||
|
f"Schema $id {schema['$id']} must be {base_uri}/{relative_path}"
|
||||||
|
|
||||||
|
schema_filename.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(pathlib.Path(path) / relative_path, "wt") as schema_file:
|
||||||
|
json.dump(schema, schema_file, indent=2)
|
||||||
|
print(f"Extracted {schema['$id']} to {schema_filename}")
|
|
@ -7,6 +7,7 @@ from .. import tracer
|
||||||
from ..hdl._ast import Shape, ShapeCastable, Const, Signal, Value, ValueCastable
|
from ..hdl._ast import Shape, ShapeCastable, Const, Signal, Value, ValueCastable
|
||||||
from ..hdl._ir import Elaboratable
|
from ..hdl._ir import Elaboratable
|
||||||
from .._utils import final
|
from .._utils import final
|
||||||
|
from .meta import Annotation, InvalidAnnotation
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["In", "Out", "Signature", "PureInterface", "connect", "flipped", "Component"]
|
__all__ = ["In", "Out", "Signature", "PureInterface", "connect", "flipped", "Component"]
|
||||||
|
@ -682,7 +683,7 @@ class Signature(metaclass=SignatureMeta):
|
||||||
An interface object is a Python object that has a :py:`signature` attribute containing
|
An interface object is a Python object that has a :py:`signature` attribute containing
|
||||||
a :class:`Signature` object, as well as an attribute for every member of its signature.
|
a :class:`Signature` object, as well as an attribute for every member of its signature.
|
||||||
Signatures and interface objects are tightly linked: an interface object can be created out
|
Signatures and interface objects are tightly linked: an interface object can be created out
|
||||||
of a signature, and the signature is used when :func:`connect` ing two interface objects
|
of a signature, and the signature is used when :func:`connect`\\ ing two interface objects
|
||||||
together. See the :ref:`introduction to interfaces <wiring-intro1>` for a more detailed
|
together. See the :ref:`introduction to interfaces <wiring-intro1>` for a more detailed
|
||||||
explanation of why this is useful.
|
explanation of why this is useful.
|
||||||
|
|
||||||
|
@ -984,6 +985,21 @@ class Signature(metaclass=SignatureMeta):
|
||||||
"""
|
"""
|
||||||
return PureInterface(self, path=path, src_loc_at=1 + src_loc_at)
|
return PureInterface(self, path=path, src_loc_at=1 + src_loc_at)
|
||||||
|
|
||||||
|
def annotations(self, obj, /):
|
||||||
|
"""Annotate an interface object.
|
||||||
|
|
||||||
|
Subclasses of :class:`Signature` may override this method to provide annotations for
|
||||||
|
a corresponding interface object. The default implementation provides none.
|
||||||
|
|
||||||
|
See :mod:`amaranth.lib.meta` for details.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
iterable of :class:`~.meta.Annotation`
|
||||||
|
:py:`tuple()`
|
||||||
|
"""
|
||||||
|
return tuple()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if type(self) is Signature:
|
if type(self) is Signature:
|
||||||
return f"Signature({dict(self.members.items())})"
|
return f"Signature({dict(self.members.items())})"
|
||||||
|
@ -1244,7 +1260,7 @@ class FlippedInterface:
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Signature
|
:class:`Signature`
|
||||||
:py:`unflipped.signature.flip()`
|
:py:`unflipped.signature.flip()`
|
||||||
"""
|
"""
|
||||||
return self.__unflipped.signature.flip()
|
return self.__unflipped.signature.flip()
|
||||||
|
@ -1254,7 +1270,7 @@ class FlippedInterface:
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
bool
|
:class:`bool`
|
||||||
:py:`True` if :py:`other` is an instance :py:`FlippedInterface(other_unflipped)` where
|
:py:`True` if :py:`other` is an instance :py:`FlippedInterface(other_unflipped)` where
|
||||||
:py:`unflipped == other_unflipped`, :py:`False` otherwise.
|
:py:`unflipped == other_unflipped`, :py:`False` otherwise.
|
||||||
"""
|
"""
|
||||||
|
@ -1676,7 +1692,7 @@ class Component(Elaboratable):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def signature(self):
|
def signature(self):
|
||||||
"""The signature of the component.
|
"""Signature of the component.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
|
@ -1685,3 +1701,244 @@ class Component(Elaboratable):
|
||||||
can be used to customize a component's signature.
|
can be used to customize a component's signature.
|
||||||
"""
|
"""
|
||||||
return self.__signature
|
return self.__signature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self):
|
||||||
|
"""Metadata attached to the component.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`ComponentMetadata`
|
||||||
|
"""
|
||||||
|
return ComponentMetadata(self)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMetadata(Exception):
|
||||||
|
"""Exception raised by :meth:`ComponentMetadata.validate` when the JSON representation of
|
||||||
|
a component's metadata does not conform to its schema."""
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentMetadata(Annotation):
|
||||||
|
"""Component metadata.
|
||||||
|
|
||||||
|
Component :ref:`metadata <meta>` describes the interface of a :class:`Component` and can be
|
||||||
|
exported to JSON for interoperability with non-Amaranth tooling.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
---------
|
||||||
|
origin : :class:`Component`
|
||||||
|
Component described by this metadata instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: :class:`dict`: Schema of component metadata, expressed in the `JSON Schema`_ language.
|
||||||
|
#:
|
||||||
|
#: A copy of this schema can be retrieved `from amaranth-lang.org
|
||||||
|
#: <https://amaranth-lang.org/schema/amaranth/0.5/component.json>`_.
|
||||||
|
schema = {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://amaranth-lang.org/schema/amaranth/0.5/component.json",
|
||||||
|
"$defs": {
|
||||||
|
"member": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "$ref": "#/$defs/member-array" },
|
||||||
|
{ "$ref": "#/$defs/member-port" },
|
||||||
|
{ "$ref": "#/$defs/member-interface" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"member-array": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/member",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"member-port": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "const": "port" },
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[A-Za-z][A-Za-z0-9_]*$",
|
||||||
|
},
|
||||||
|
"dir": { "enum": [ "in", "out" ] },
|
||||||
|
"width": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
},
|
||||||
|
"signed": { "type": "boolean" },
|
||||||
|
"init": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[+-]?[0-9]+$",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"name",
|
||||||
|
"dir",
|
||||||
|
"width",
|
||||||
|
"signed",
|
||||||
|
"init",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"member-interface": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": { "const": "interface" },
|
||||||
|
"members": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[A-Za-z][A-Za-z0-9_]*$": {
|
||||||
|
"$ref": "#/$defs/member",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"members",
|
||||||
|
"annotations",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interface": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"members": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[A-Za-z][A-Za-z0-9_]*$": {
|
||||||
|
"$ref": "#/$defs/member",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"required": [
|
||||||
|
"members",
|
||||||
|
"annotations",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"required": [
|
||||||
|
"interface",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, origin):
|
||||||
|
if not isinstance(origin, Component):
|
||||||
|
raise TypeError(f"Origin must be a component, not {origin!r}")
|
||||||
|
self._origin = origin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def origin(self):
|
||||||
|
"""Component described by this metadata.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`Component`
|
||||||
|
"""
|
||||||
|
return self._origin
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, instance):
|
||||||
|
"""Validate a JSON representation of component metadata against :attr:`schema`.
|
||||||
|
|
||||||
|
This method does not validate annotations of the interface members, and consequently does
|
||||||
|
not make network requests.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
---------
|
||||||
|
instance : :class:`dict`
|
||||||
|
JSON representation to validate, either previously returned by :meth:`as_json` or
|
||||||
|
retrieved from an external source.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
:exc:`InvalidMetadata`
|
||||||
|
If :py:`instance` doesn't conform to :attr:`schema`.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
super(cls, cls).validate(instance)
|
||||||
|
except InvalidAnnotation as e:
|
||||||
|
raise InvalidMetadata(e) from e
|
||||||
|
|
||||||
|
def as_json(self):
|
||||||
|
"""Translate to JSON.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
:class:`dict`
|
||||||
|
JSON representation of :attr:`origin` that describes its interface members and includes
|
||||||
|
their annotations.
|
||||||
|
"""
|
||||||
|
def translate_member(member, origin, *, path):
|
||||||
|
assert isinstance(member, Member)
|
||||||
|
if member.is_port:
|
||||||
|
cast_shape = Shape.cast(member.shape)
|
||||||
|
return {
|
||||||
|
"type": "port",
|
||||||
|
"name": "__".join(str(key) for key in path),
|
||||||
|
"dir": "in" if member.flow == In else "out",
|
||||||
|
"width": cast_shape.width,
|
||||||
|
"signed": cast_shape.signed,
|
||||||
|
"init": str(member._init_as_const.value),
|
||||||
|
}
|
||||||
|
elif member.is_signature:
|
||||||
|
return {
|
||||||
|
"type": "interface",
|
||||||
|
"members": {
|
||||||
|
sub_name: translate_dimensions(sub.dimensions, sub,
|
||||||
|
getattr(origin, sub_name),
|
||||||
|
path=(*path, sub_name))
|
||||||
|
for sub_name, sub in member.signature.members.items()
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
annotation.schema["$id"]: annotation.as_json()
|
||||||
|
for annotation in member.signature.annotations(origin)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
assert False # :nocov:
|
||||||
|
|
||||||
|
def translate_dimensions(dimensions, member, origin, *, path):
|
||||||
|
if len(dimensions) == 0:
|
||||||
|
return translate_member(member, origin, path=path)
|
||||||
|
dimension, *rest_of_dimensions = dimensions
|
||||||
|
return [
|
||||||
|
translate_dimensions(rest_of_dimensions, member, origin[index],
|
||||||
|
path=(*path, index))
|
||||||
|
for index in range(dimension)
|
||||||
|
]
|
||||||
|
|
||||||
|
instance = {
|
||||||
|
"interface": {
|
||||||
|
"members": {
|
||||||
|
member_name: translate_dimensions(member.dimensions, member,
|
||||||
|
getattr(self.origin, member_name),
|
||||||
|
path=(member_name,))
|
||||||
|
for member_name, member in self.origin.signature.members.items()
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
annotation.schema["$id"]: annotation.as_json()
|
||||||
|
for annotation in self.origin.signature.annotations(self.origin)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.validate(instance)
|
||||||
|
return instance
|
||||||
|
|
|
@ -49,6 +49,7 @@ Implemented RFCs
|
||||||
|
|
||||||
.. _RFC 17: https://amaranth-lang.org/rfcs/0017-remove-log2-int.html
|
.. _RFC 17: https://amaranth-lang.org/rfcs/0017-remove-log2-int.html
|
||||||
.. _RFC 27: https://amaranth-lang.org/rfcs/0027-simulator-testbenches.html
|
.. _RFC 27: https://amaranth-lang.org/rfcs/0027-simulator-testbenches.html
|
||||||
|
.. _RFC 30: https://amaranth-lang.org/rfcs/0030-component-metadata.html
|
||||||
.. _RFC 39: https://amaranth-lang.org/rfcs/0039-empty-case.html
|
.. _RFC 39: https://amaranth-lang.org/rfcs/0039-empty-case.html
|
||||||
.. _RFC 43: https://amaranth-lang.org/rfcs/0043-rename-reset-to-init.html
|
.. _RFC 43: https://amaranth-lang.org/rfcs/0043-rename-reset-to-init.html
|
||||||
.. _RFC 45: https://amaranth-lang.org/rfcs/0045-lib-memory.html
|
.. _RFC 45: https://amaranth-lang.org/rfcs/0045-lib-memory.html
|
||||||
|
@ -65,6 +66,7 @@ Implemented RFCs
|
||||||
|
|
||||||
* `RFC 17`_: Remove ``log2_int``
|
* `RFC 17`_: Remove ``log2_int``
|
||||||
* `RFC 27`_: Testbench processes for the simulator
|
* `RFC 27`_: Testbench processes for the simulator
|
||||||
|
* `RFC 30`_: Component metadata
|
||||||
* `RFC 39`_: Change semantics of no-argument ``m.Case()``
|
* `RFC 39`_: Change semantics of no-argument ``m.Case()``
|
||||||
* `RFC 43`_: Rename ``reset=`` to ``init=``
|
* `RFC 43`_: Rename ``reset=`` to ``init=``
|
||||||
* `RFC 45`_: Move ``hdl.Memory`` to ``lib.Memory``
|
* `RFC 45`_: Move ``hdl.Memory`` to ``lib.Memory``
|
||||||
|
@ -120,6 +122,7 @@ Standard library changes
|
||||||
* Changed: :meth:`amaranth.lib.wiring.Signature.is_compliant` no longer rejects reset-less signals.
|
* Changed: :meth:`amaranth.lib.wiring.Signature.is_compliant` no longer rejects reset-less signals.
|
||||||
* Added: :class:`amaranth.lib.io.SingleEndedPort`, :class:`amaranth.lib.io.DifferentialPort`. (`RFC 55`_)
|
* Added: :class:`amaranth.lib.io.SingleEndedPort`, :class:`amaranth.lib.io.DifferentialPort`. (`RFC 55`_)
|
||||||
* Added: :class:`amaranth.lib.io.Buffer`, :class:`amaranth.lib.io.FFBuffer`, :class:`amaranth.lib.io.DDRBuffer`. (`RFC 55`_)
|
* Added: :class:`amaranth.lib.io.Buffer`, :class:`amaranth.lib.io.FFBuffer`, :class:`amaranth.lib.io.DDRBuffer`. (`RFC 55`_)
|
||||||
|
* Added: :mod:`amaranth.lib.meta`, :class:`amaranth.lib.wiring.ComponentMetadata`. (`RFC 30`_)
|
||||||
* Deprecated: :mod:`amaranth.lib.coding`. (`RFC 63`_)
|
* Deprecated: :mod:`amaranth.lib.coding`. (`RFC 63`_)
|
||||||
* Removed: (deprecated in 0.4) :mod:`amaranth.lib.scheduler`. (`RFC 19`_)
|
* 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`_)
|
* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.FIFOInterface` with ``fwft=False``. (`RFC 20`_)
|
||||||
|
|
|
@ -2,10 +2,11 @@ import os, sys
|
||||||
sys.path.insert(0, os.path.abspath("."))
|
sys.path.insert(0, os.path.abspath("."))
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import amaranth
|
from importlib.metadata import version as package_version
|
||||||
|
|
||||||
|
|
||||||
project = "Amaranth language & toolchain"
|
project = "Amaranth language & toolchain"
|
||||||
version = amaranth.__version__.replace(".editable", "")
|
version = package_version('amaranth').replace(".editable", "")
|
||||||
release = version.split("+")[0]
|
release = version.split("+")[0]
|
||||||
copyright = time.strftime("2020—%Y, Amaranth project contributors")
|
copyright = time.strftime("2020—%Y, Amaranth project contributors")
|
||||||
|
|
||||||
|
@ -25,7 +26,9 @@ with open(".gitignore") as f:
|
||||||
|
|
||||||
root_doc = "cover"
|
root_doc = "cover"
|
||||||
|
|
||||||
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
|
intersphinx_mapping = {
|
||||||
|
"python": ("https://docs.python.org/3", None),
|
||||||
|
}
|
||||||
|
|
||||||
todo_include_todos = True
|
todo_include_todos = True
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ Standard library
|
||||||
|
|
||||||
The :mod:`amaranth.lib` module, also known as the standard library, provides modules that falls into one of the three categories:
|
The :mod:`amaranth.lib` module, also known as the standard library, provides modules that falls into one of the three categories:
|
||||||
|
|
||||||
1. Modules that will used by essentially all idiomatic Amaranth code, and are necessary for interoperability. This includes :mod:`amaranth.lib.enum` (enumerations), :mod:`amaranth.lib.data` (data structures), and :mod:`amaranth.lib.wiring` (interfaces and components).
|
1. Modules that will used by essentially all idiomatic Amaranth code, or which are necessary for interoperability. This includes :mod:`amaranth.lib.enum` (enumerations), :mod:`amaranth.lib.data` (data structures), :mod:`amaranth.lib.wiring` (interfaces and components), and :mod:`amaranth.lib.meta` (interface metadata).
|
||||||
2. Modules that abstract common functionality whose implementation differs between hardware platforms. This includes :mod:`amaranth.lib.cdc`, :mod:`amaranth.lib.memory`.
|
2. Modules that abstract common functionality whose implementation differs between hardware platforms. This includes :mod:`amaranth.lib.cdc`, :mod:`amaranth.lib.memory`.
|
||||||
3. Modules that have essentially one correct implementation and are of broad utility in digital designs. This includes :mod:`amaranth.lib.coding`, :mod:`amaranth.lib.fifo`, and :mod:`amaranth.lib.crc`.
|
3. Modules that have essentially one correct implementation and are of broad utility in digital designs. This includes :mod:`amaranth.lib.coding`, :mod:`amaranth.lib.fifo`, and :mod:`amaranth.lib.crc`.
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ The Amaranth standard library is separate from the Amaranth language: everything
|
||||||
stdlib/enum
|
stdlib/enum
|
||||||
stdlib/data
|
stdlib/data
|
||||||
stdlib/wiring
|
stdlib/wiring
|
||||||
|
stdlib/meta
|
||||||
stdlib/memory
|
stdlib/memory
|
||||||
stdlib/cdc
|
stdlib/cdc
|
||||||
stdlib/coding
|
stdlib/coding
|
||||||
|
|
285
docs/stdlib/meta.rst
Normal file
285
docs/stdlib/meta.rst
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
.. _meta:
|
||||||
|
|
||||||
|
Interface metadata
|
||||||
|
##################
|
||||||
|
|
||||||
|
.. py:module:: amaranth.lib.meta
|
||||||
|
|
||||||
|
The :mod:`amaranth.lib.meta` module provides a way to annotate objects in an Amaranth design and exchange these annotations with external tools in a standardized format.
|
||||||
|
|
||||||
|
.. _JSON Schema: https://json-schema.org
|
||||||
|
|
||||||
|
.. _"$id" keyword: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#name-the-id-keyword
|
||||||
|
|
||||||
|
.. testsetup::
|
||||||
|
|
||||||
|
from amaranth import *
|
||||||
|
from amaranth.lib import wiring, meta
|
||||||
|
from amaranth.lib.wiring import In, Out
|
||||||
|
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
Many Amaranth designs stay entirely within the Amaranth ecosystem, using the facilities it provides to define, test, and build hardware. In this case, the design is available for exploration using Python code, and metadata is not necessary. However, if an Amaranth design needs to fit into an existing ecosystem, or, conversely, to integrate components developed for another ecosystem, metadata can be used to exchange structured information about the design.
|
||||||
|
|
||||||
|
Consider a simple :ref:`component <wiring>`:
|
||||||
|
|
||||||
|
.. testcode::
|
||||||
|
|
||||||
|
class Adder(wiring.Component):
|
||||||
|
a: In(unsigned(32))
|
||||||
|
b: In(unsigned(32))
|
||||||
|
o: Out(unsigned(33))
|
||||||
|
|
||||||
|
def elaborate(self, platform):
|
||||||
|
m = Module()
|
||||||
|
m.d.comb += self.o.eq(self.a + self.b)
|
||||||
|
return m
|
||||||
|
|
||||||
|
..
|
||||||
|
TODO: link to Verilog backend doc when we have it
|
||||||
|
|
||||||
|
While it can be easily converted to Verilog, external tools will find the interface of the resulting module opaque unless they parse its Verilog source (a difficult and unrewarding task), or are provided with a description of it. Components can describe their signature with JSON-based metadata:
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> adder = Adder()
|
||||||
|
>>> adder.metadata # doctest: +ELLIPSIS
|
||||||
|
<amaranth.lib.wiring.ComponentMetadata for ...Adder object at ...>
|
||||||
|
>>> adder.metadata.as_json() # doctest: +SKIP
|
||||||
|
{
|
||||||
|
'interface': {
|
||||||
|
'members': {
|
||||||
|
'a': {
|
||||||
|
'type': 'port',
|
||||||
|
'name': 'a',
|
||||||
|
'dir': 'in',
|
||||||
|
'width': 32,
|
||||||
|
'signed': False,
|
||||||
|
'init': '0'
|
||||||
|
},
|
||||||
|
'b': {
|
||||||
|
'type': 'port',
|
||||||
|
'name': 'b',
|
||||||
|
'dir': 'in',
|
||||||
|
'width': 32,
|
||||||
|
'signed': False,
|
||||||
|
'init': '0'
|
||||||
|
},
|
||||||
|
'o': {
|
||||||
|
'type': 'port',
|
||||||
|
'name': 'o',
|
||||||
|
'dir': 'out',
|
||||||
|
'width': 33,
|
||||||
|
'signed': False,
|
||||||
|
'init': '0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'annotations': {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. testcode::
|
||||||
|
:hide:
|
||||||
|
|
||||||
|
# The way doctest requires this object to be formatted is truly hideous, even with +NORMALIZE_WHITESPACE.
|
||||||
|
assert adder.metadata.as_json() == {'interface': {'members': {'a': {'type': 'port', 'name': 'a', 'dir': 'in', 'width': 32, 'signed': False, 'init': '0'}, 'b': {'type': 'port', 'name': 'b', 'dir': 'in', 'width': 32, 'signed': False, 'init': '0'}, 'o': {'type': 'port', 'name': 'o', 'dir': 'out', 'width': 33, 'signed': False, 'init': '0'}}, 'annotations': {}}}
|
||||||
|
|
||||||
|
|
||||||
|
All metadata in Amaranth must adhere to a schema in the `JSON Schema`_ language, which is integral to its definition, and can be used to validate the generated JSON:
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> wiring.ComponentMetadata.validate(adder.metadata.as_json())
|
||||||
|
|
||||||
|
The built-in component metadata can be extended to provide arbitrary information about an interface through user-defined annotations. For example, a memory bus interface could provide the layout of any memory-mapped peripherals accessible through that bus.
|
||||||
|
|
||||||
|
|
||||||
|
Defining annotations
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Consider a simple control and status register (CSR) bus that provides the memory layout of the accessible registers via an annotation:
|
||||||
|
|
||||||
|
.. testcode::
|
||||||
|
|
||||||
|
class CSRLayoutAnnotation(meta.Annotation):
|
||||||
|
schema = {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://amaranth-lang.org/schema/example/0/csr-layout.json",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"registers": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^.+$": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"requiredProperties": [
|
||||||
|
"registers",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, origin):
|
||||||
|
self._origin = origin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def origin(self):
|
||||||
|
return self._origin
|
||||||
|
|
||||||
|
def as_json(self):
|
||||||
|
instance = {
|
||||||
|
"registers": self.origin.registers,
|
||||||
|
}
|
||||||
|
# Validating the value returned by `as_json()` ensures its conformance.
|
||||||
|
self.validate(instance)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class CSRSignature(wiring.Signature):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__({
|
||||||
|
"addr": Out(16),
|
||||||
|
"w_en": Out(1),
|
||||||
|
"w_data": Out(32),
|
||||||
|
"r_en": Out(1),
|
||||||
|
"r_data": In(32),
|
||||||
|
})
|
||||||
|
|
||||||
|
def annotations(self, obj, /):
|
||||||
|
# Unfortunately `super()` cannot be used in `wiring.Signature` subclasses;
|
||||||
|
# instead, use a direct call to a superclass method. In this case that is
|
||||||
|
# `wiring.Signature` itself, but in a more complex class hierarchy it could
|
||||||
|
# be different.
|
||||||
|
return wiring.Signature.annotations(self, obj) + (CSRLayoutAnnotation(obj),)
|
||||||
|
|
||||||
|
A component that embeds a few CSR registers would define their addresses:
|
||||||
|
|
||||||
|
.. testcode::
|
||||||
|
|
||||||
|
class MyPeripheral(wiring.Component):
|
||||||
|
csr_bus: In(CSRSignature())
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.csr_bus.registers = {
|
||||||
|
"control": 0x0000,
|
||||||
|
"status": 0x0004,
|
||||||
|
"data": 0x0008,
|
||||||
|
}
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> peripheral = MyPeripheral()
|
||||||
|
>>> peripheral.metadata.as_json() # doctest: +SKIP
|
||||||
|
{
|
||||||
|
'interface': {
|
||||||
|
'members': {
|
||||||
|
'csr_bus': {
|
||||||
|
'type': 'interface',
|
||||||
|
'members': {
|
||||||
|
'addr': {
|
||||||
|
'type': 'port',
|
||||||
|
'name': 'csr_bus__addr',
|
||||||
|
'dir': 'in',
|
||||||
|
'width': 16,
|
||||||
|
'signed': False,
|
||||||
|
'init': '0'
|
||||||
|
},
|
||||||
|
'w_en': {
|
||||||
|
'type': 'port',
|
||||||
|
'name': 'csr_bus__w_en',
|
||||||
|
'dir': 'in',
|
||||||
|
'width': 1,
|
||||||
|
'signed': False,
|
||||||
|
'init': '0'
|
||||||
|
},
|
||||||
|
'w_data': {
|
||||||
|
'type': 'port',
|
||||||
|
'name': 'csr_bus__w_data',
|
||||||
|
'dir': 'in',
|
||||||
|
'width': 32,
|
||||||
|
'signed': False,
|
||||||
|
'init': '0'
|
||||||
|
},
|
||||||
|
'r_en': {
|
||||||
|
'type': 'port',
|
||||||
|
'name': 'csr_bus__r_en',
|
||||||
|
'dir': 'in',
|
||||||
|
'width': 1,
|
||||||
|
'signed': False,
|
||||||
|
'init': '0'
|
||||||
|
},
|
||||||
|
'r_data': {
|
||||||
|
'type': 'port',
|
||||||
|
'name': 'csr_bus__r_data',
|
||||||
|
'dir': 'out',
|
||||||
|
'width': 32,
|
||||||
|
'signed': False,
|
||||||
|
'init': '0'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'annotations': {
|
||||||
|
'https://amaranth-lang.org/schema/example/0/csr-layout.json': {
|
||||||
|
'registers': {
|
||||||
|
'control': 0,
|
||||||
|
'status': 4,
|
||||||
|
'data': 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'annotations': {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. testcode::
|
||||||
|
:hide:
|
||||||
|
|
||||||
|
# The way doctest requires this object to be formatted is truly hideous, even with +NORMALIZE_WHITESPACE.
|
||||||
|
assert peripheral.metadata.as_json() == {'interface': {'members': {'csr_bus': {'type': 'interface', 'members': {'addr': {'type': 'port', 'name': 'csr_bus__addr', 'dir': 'in', 'width': 16, 'signed': False, 'init': '0'}, 'w_en': {'type': 'port', 'name': 'csr_bus__w_en', 'dir': 'in', 'width': 1, 'signed': False, 'init': '0'}, 'w_data': {'type': 'port', 'name': 'csr_bus__w_data', 'dir': 'in', 'width': 32, 'signed': False, 'init': '0'}, 'r_en': {'type': 'port', 'name': 'csr_bus__r_en', 'dir': 'in', 'width': 1, 'signed': False, 'init': '0'}, 'r_data': {'type': 'port', 'name': 'csr_bus__r_data', 'dir': 'out', 'width': 32, 'signed': False, 'init': '0'}}, 'annotations': {'https://amaranth-lang.org/schema/example/0/csr-layout.json': {'registers': {'control': 0, 'status': 4, 'data': 8}}}}}, 'annotations': {}}}
|
||||||
|
|
||||||
|
|
||||||
|
Identifying schemas
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
An :class:`Annotation` schema must have a ``"$id"`` property, whose value is a URL that serves as its globally unique identifier. The suggested format of this URL is:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
<protocol>://<domain>/schema/<package>/<version>/<path>.json
|
||||||
|
|
||||||
|
where:
|
||||||
|
|
||||||
|
* ``<domain>`` is a domain name registered to the person or entity defining the annotation;
|
||||||
|
* ``<package>`` is the name of the Python package providing the :class:`Annotation` subclass;
|
||||||
|
* ``<version>`` is the version of that package;
|
||||||
|
* ``<path>`` is a non-empty string specific to the annotation.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Annotations used in the Amaranth project packages are published under https://amaranth-lang.org/schema/ according to this URL format, and are covered by the usual compatibility commitment.
|
||||||
|
|
||||||
|
Other projects that define additional Amaranth annotations are encouraged, but not required, to make their schemas publicly accessible; the only requirement is for the URL to be globally unique.
|
||||||
|
|
||||||
|
|
||||||
|
Reference
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. autoexception:: InvalidSchema
|
||||||
|
|
||||||
|
.. autoexception:: InvalidAnnotation
|
||||||
|
|
||||||
|
.. autoclass:: Annotation
|
||||||
|
:no-members:
|
||||||
|
:members: validate, origin, as_json
|
||||||
|
|
||||||
|
.. automethod:: __init_subclass__()
|
||||||
|
|
||||||
|
.. autoattribute:: schema
|
||||||
|
:annotation: = { "$id": "...", ... }
|
|
@ -599,4 +599,19 @@ Making connections
|
||||||
Components
|
Components
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
.. _JSON Schema: https://json-schema.org
|
||||||
|
|
||||||
.. autoclass:: Component
|
.. autoclass:: Component
|
||||||
|
|
||||||
|
|
||||||
|
Component metadata
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. autoexception:: InvalidMetadata
|
||||||
|
|
||||||
|
.. autoclass:: ComponentMetadata
|
||||||
|
:no-members:
|
||||||
|
:members: validate, origin, as_json
|
||||||
|
|
||||||
|
.. autoattribute:: schema
|
||||||
|
:annotation: = { "$id": "https://amaranth-lang.org/schema/amaranth/0.5/component.json", ... }
|
||||||
|
|
|
@ -16,6 +16,7 @@ license = {file = "LICENSE.txt"}
|
||||||
requires-python = "~=3.8"
|
requires-python = "~=3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"importlib_resources; python_version<'3.9'", # for amaranth._toolchain.yosys
|
"importlib_resources; python_version<'3.9'", # for amaranth._toolchain.yosys
|
||||||
|
"jschon~=0.11.1", # for amaranth.lib.meta
|
||||||
"pyvcd>=0.2.2,<0.5", # for amaranth.sim.pysim
|
"pyvcd>=0.2.2,<0.5", # for amaranth.sim.pysim
|
||||||
"Jinja2~=3.0", # for amaranth.build
|
"Jinja2~=3.0", # for amaranth.build
|
||||||
]
|
]
|
||||||
|
@ -31,6 +32,9 @@ remote-build = ["paramiko~=2.7"]
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
amaranth-rpc = "amaranth.rpc:main"
|
amaranth-rpc = "amaranth.rpc:main"
|
||||||
|
|
||||||
|
[project.entry-points."amaranth.lib.meta"]
|
||||||
|
"0.5/component.json" = "amaranth.lib.wiring:ComponentMetadata"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
"Homepage" = "https://amaranth-lang.org/"
|
"Homepage" = "https://amaranth-lang.org/"
|
||||||
"Documentation" = "https://amaranth-lang.org/docs/amaranth/" # modified in pdm_build.py
|
"Documentation" = "https://amaranth-lang.org/docs/amaranth/" # modified in pdm_build.py
|
||||||
|
@ -95,3 +99,5 @@ document-linkcheck.cmd = "sphinx-build docs/ docs/_linkcheck/ -b linkcheck"
|
||||||
coverage-text.cmd = "python -m coverage report"
|
coverage-text.cmd = "python -m coverage report"
|
||||||
coverage-html.cmd = "python -m coverage html"
|
coverage-html.cmd = "python -m coverage html"
|
||||||
coverage-xml.cmd = "python -m coverage xml"
|
coverage-xml.cmd = "python -m coverage xml"
|
||||||
|
|
||||||
|
extract-schemas.call = "amaranth.lib.meta:_extract_schemas('amaranth', base_uri='https://amaranth-lang.org/schema/amaranth')"
|
||||||
|
|
87
tests/test_lib_meta.py
Normal file
87
tests/test_lib_meta.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from amaranth import *
|
||||||
|
from amaranth.lib.meta import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationTestCase(unittest.TestCase):
|
||||||
|
def test_init_subclass(self):
|
||||||
|
class MyAnnotation(Annotation):
|
||||||
|
schema = {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
|
||||||
|
"type": "string",
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def origin(self):
|
||||||
|
return "foo"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def as_json(self):
|
||||||
|
return "foo"
|
||||||
|
|
||||||
|
self.assertRegex(repr(MyAnnotation()), r"<.+\.MyAnnotation for 'foo'>")
|
||||||
|
|
||||||
|
def test_init_subclass_wrong_schema(self):
|
||||||
|
with self.assertRaisesRegex(TypeError, r"Annotation schema must be a dict, not 'foo'"):
|
||||||
|
class MyAnnotation(Annotation):
|
||||||
|
schema = "foo"
|
||||||
|
|
||||||
|
def test_init_subclass_schema_missing_id(self):
|
||||||
|
with self.assertRaisesRegex(InvalidSchema, r"'\$id' keyword is missing from Annotation schema: {}"):
|
||||||
|
class MyAnnotation(Annotation):
|
||||||
|
schema = {}
|
||||||
|
|
||||||
|
def test_init_subclass_schema_missing_schema(self):
|
||||||
|
with self.assertRaises(InvalidSchema):
|
||||||
|
class MyAnnotation(Annotation):
|
||||||
|
schema = {
|
||||||
|
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_init_subclass_schema_error(self):
|
||||||
|
with self.assertRaises(InvalidSchema):
|
||||||
|
class MyAnnotation(Annotation):
|
||||||
|
schema = {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
|
||||||
|
"type": "foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_validate(self):
|
||||||
|
class MyAnnotation(Annotation):
|
||||||
|
schema = {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"foo": {
|
||||||
|
"enum": [ "bar" ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"required": [
|
||||||
|
"foo",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
MyAnnotation.validate({"foo": "bar"})
|
||||||
|
|
||||||
|
def test_validate_error(self):
|
||||||
|
class MyAnnotation(Annotation):
|
||||||
|
schema = {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://example.com/schema/test/0.1/my-annotation.json",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"foo": {
|
||||||
|
"enum": [ "bar" ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"required": [
|
||||||
|
"foo",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
with self.assertRaises(InvalidAnnotation):
|
||||||
|
MyAnnotation.validate({"foo": "baz"})
|
|
@ -8,8 +8,9 @@ from amaranth.lib import data, enum
|
||||||
from amaranth.lib.wiring import Flow, In, Out, Member
|
from amaranth.lib.wiring import Flow, In, Out, Member
|
||||||
from amaranth.lib.wiring import SignatureError, SignatureMembers, FlippedSignatureMembers
|
from amaranth.lib.wiring import SignatureError, SignatureMembers, FlippedSignatureMembers
|
||||||
from amaranth.lib.wiring import Signature, FlippedSignature, PureInterface, FlippedInterface
|
from amaranth.lib.wiring import Signature, FlippedSignature, PureInterface, FlippedInterface
|
||||||
from amaranth.lib.wiring import Component
|
from amaranth.lib.wiring import Component, ComponentMetadata, InvalidMetadata
|
||||||
from amaranth.lib.wiring import ConnectionError, connect, flipped
|
from amaranth.lib.wiring import ConnectionError, connect, flipped
|
||||||
|
from amaranth.lib.meta import Annotation
|
||||||
|
|
||||||
|
|
||||||
class FlowTestCase(unittest.TestCase):
|
class FlowTestCase(unittest.TestCase):
|
||||||
|
@ -336,6 +337,11 @@ class SignatureTestCase(unittest.TestCase):
|
||||||
sig = Signature({"a": In(1)})
|
sig = Signature({"a": In(1)})
|
||||||
self.assertEqual(sig.members, SignatureMembers({"a": In(1)}))
|
self.assertEqual(sig.members, SignatureMembers({"a": In(1)}))
|
||||||
|
|
||||||
|
def test_annotations_empty(self):
|
||||||
|
sig = Signature({"a": In(1)})
|
||||||
|
iface = PureInterface(sig)
|
||||||
|
self.assertEqual(sig.annotations(iface), ())
|
||||||
|
|
||||||
def test_eq(self):
|
def test_eq(self):
|
||||||
self.assertEqual(Signature({"a": In(1)}),
|
self.assertEqual(Signature({"a": In(1)}),
|
||||||
Signature({"a": In(1)}))
|
Signature({"a": In(1)}))
|
||||||
|
@ -1156,3 +1162,174 @@ class ComponentTestCase(unittest.TestCase):
|
||||||
with self.assertRaisesRegex(TypeError,
|
with self.assertRaisesRegex(TypeError,
|
||||||
r"^Object 4 is not a signature nor a dict$"):
|
r"^Object 4 is not a signature nor a dict$"):
|
||||||
C(2)
|
C(2)
|
||||||
|
|
||||||
|
def test_metadata_origin(self):
|
||||||
|
class A(Component):
|
||||||
|
clk: In(1)
|
||||||
|
|
||||||
|
a = A()
|
||||||
|
self.assertIsInstance(a.metadata, ComponentMetadata)
|
||||||
|
self.assertIs(a.metadata.origin, a)
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentMetadataTestCase(unittest.TestCase):
|
||||||
|
def test_as_json(self):
|
||||||
|
class Annotation1(Annotation):
|
||||||
|
schema = {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://example.com/schema/foo/0.1/bar.json",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hello": { "type": "boolean" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def origin(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
def as_json(self):
|
||||||
|
instance = { "hello": True }
|
||||||
|
self.validate(instance)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
class Signature1(Signature):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__({
|
||||||
|
"i": In(unsigned(8), init=42).array(2).array(3),
|
||||||
|
"o": Out(signed(4))
|
||||||
|
})
|
||||||
|
|
||||||
|
def annotations(self, obj):
|
||||||
|
return (*Signature.annotations(self, obj), Annotation1())
|
||||||
|
|
||||||
|
class Signature2(Signature):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__({
|
||||||
|
"clk": In(1),
|
||||||
|
"foo": Out(Signature1()).array(4),
|
||||||
|
"oof": In(Signature1())
|
||||||
|
})
|
||||||
|
|
||||||
|
def annotations(self, obj):
|
||||||
|
return (*Signature.annotations(self, obj), Annotation1())
|
||||||
|
|
||||||
|
class A(Component):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(Signature2())
|
||||||
|
|
||||||
|
metadata = ComponentMetadata(A())
|
||||||
|
self.assertEqual(metadata.as_json(), {
|
||||||
|
"interface": {
|
||||||
|
"members": {
|
||||||
|
"clk": {
|
||||||
|
"type": "port",
|
||||||
|
"name": "clk",
|
||||||
|
"dir": "in",
|
||||||
|
"width": 1,
|
||||||
|
"signed": False,
|
||||||
|
"init": "0",
|
||||||
|
},
|
||||||
|
"foo": [{
|
||||||
|
"type": "interface",
|
||||||
|
"members": {
|
||||||
|
"i": [[{
|
||||||
|
"type": "port",
|
||||||
|
"name": f"foo__{x}__i__{y}__{z}",
|
||||||
|
"dir": "in",
|
||||||
|
"width": 8,
|
||||||
|
"signed": False,
|
||||||
|
"init": "42",
|
||||||
|
} for z in range(2)] for y in range(3)],
|
||||||
|
"o": {
|
||||||
|
"type": "port",
|
||||||
|
"name": f"foo__{x}__o",
|
||||||
|
"dir": "out",
|
||||||
|
"width": 4,
|
||||||
|
"signed": True,
|
||||||
|
"init": "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"https://example.com/schema/foo/0.1/bar.json": {
|
||||||
|
"hello": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} for x in range(4)],
|
||||||
|
"oof": {
|
||||||
|
"type": "interface",
|
||||||
|
"members": {
|
||||||
|
"i": [[{
|
||||||
|
"type": "port",
|
||||||
|
"name": f"oof__i__{y}__{z}",
|
||||||
|
"dir": "out",
|
||||||
|
"width": 8,
|
||||||
|
"signed": False,
|
||||||
|
"init": "42",
|
||||||
|
} for z in range(2)] for y in range(3)],
|
||||||
|
"o": {
|
||||||
|
"type": "port",
|
||||||
|
"name": "oof__o",
|
||||||
|
"dir": "in",
|
||||||
|
"width": 4,
|
||||||
|
"signed": True,
|
||||||
|
"init": "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"https://example.com/schema/foo/0.1/bar.json": {
|
||||||
|
"hello": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"https://example.com/schema/foo/0.1/bar.json": {
|
||||||
|
"hello": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_validate(self):
|
||||||
|
ComponentMetadata.validate({
|
||||||
|
"interface": {
|
||||||
|
"members": {
|
||||||
|
"i": {
|
||||||
|
"type": "port",
|
||||||
|
"name": "i",
|
||||||
|
"dir": "in",
|
||||||
|
"width": 1,
|
||||||
|
"signed": False,
|
||||||
|
"init": "0",
|
||||||
|
},
|
||||||
|
"o": {
|
||||||
|
"type": "port",
|
||||||
|
"name": "o",
|
||||||
|
"dir": "out",
|
||||||
|
"width": 1,
|
||||||
|
"signed": False,
|
||||||
|
"init": "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"https://example.com/schema/foo/0/foo.json": {
|
||||||
|
"foo": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_validate_error(self):
|
||||||
|
with self.assertRaises(InvalidMetadata):
|
||||||
|
ComponentMetadata.validate({
|
||||||
|
"interface": {
|
||||||
|
"members": {
|
||||||
|
"foo": True,
|
||||||
|
},
|
||||||
|
"annotations": {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_wrong_origin(self):
|
||||||
|
with self.assertRaisesRegex(TypeError, r"Origin must be a component, not 'foo'"):
|
||||||
|
ComponentMetadata("foo")
|
||||||
|
|
Loading…
Reference in a new issue