# Interface metadata

The `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.

## 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 component:

    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

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:

    >>> adder = Adder()
    >>> adder.metadata
    <amaranth.lib.wiring.ComponentMetadata for ...Adder object at ...>
    >>> 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:

    >>> 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:

    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:

    class MyPeripheral(wiring.Component):
        csr_bus: In(CSRSignature())

        def __init__(self):
            super().__init__()
            self.csr_bus.registers = {
                "control": 0x0000,
                "status":  0x0004,
                "data":    0x0008,
            }

    >>> peripheral = MyPeripheral()
    >>> 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 `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:

    <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 `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

exception amaranth.lib.meta.InvalidSchema

  Exception raised when a subclass of `Annotation` is defined with a non-conformant `schema`.

exception amaranth.lib.meta.InvalidAnnotation

  Exception raised by `Annotation.validate()` when the JSON representation of an annotation does not conform to its schema.

class amaranth.lib.meta.Annotation

  Interface annotation.

  Annotations are containers for metadata that can be retrieved from an interface object using the `Signature.annotations` method.

  Annotations have a JSON representation whose structure is defined by the JSON Schema language.

  classmethod __init_subclass__()

    Defining a subclass of `Annotation` causes its `schema` to be validated.

    Raises:

      - InvalidSchema – If `schema` doesn’t conform to the 2020-12 draft of JSON Schema.

      - InvalidSchema – If `schema` doesn’t have a “$id” keyword at its root. This requirement is specific to `Annotation` schemas.

  schema = { "$id": "...", ... }

    Schema of this annotation, expressed in the JSON Schema language.

    Subclasses of `Annotation` must define this class attribute.

    Type:

      `dict`

  abstract property origin

    Python object described by this `Annotation` instance.

    Subclasses of `Annotation` must implement this property.

  abstract as_json()

    Convert to a JSON representation.

    Subclasses of `Annotation` must implement this method.

    JSON representation returned by this method must adhere to `schema` and pass validation by `validate()`.

    Returns:

      JSON representation of this annotation, expressed in Python primitive types (`dict`, `list`, `str`, `int`, `bool`).

    Return type:

      `dict`

  classmethod validate(instance)

    Validate a JSON representation against `schema`.

    Parameters:

      instance (`dict`) – JSON representation to validate, either previously returned by `as_json()` or retrieved from an external source.

    Raises:

      InvalidAnnotation – If `instance` doesn’t conform to `schema`.