Skip to content

math_spec.serialisation

The waist as a value another language can read.

A consumer written in Python imports :class:~math_spec.program.Program and is done. A consumer written in anything else has, until now, had one way to reach the same model: implement the language a second time — a second parser, a second name resolution, a second dim algebra — which is the one thing hard rule 1 exists to forbid. Two implementations of name resolution is the failure the whole design is built to prevent, and shipping the waist only as importable Python quietly guarantees it.

So the program is written as JSON, and read back from it. Everything in a program is already resolved, so the form carries no grammar, no expression strings and nothing to parse: a reader dispatches on a tag and builds a node.

Every node is tagged with its class name and nothing else is. A dataclass or named tuple becomes an object carrying $; a mapping becomes an object without one; a tuple becomes an array. That is the whole encoding, and it is reflective rather than hand-written so a node added to the program is serialisable the day it exists — tests/test_program_nodes.py already refuses a node no file reaches, so the round trip covers every node by construction.

Reading is closed. A tag is looked up in a registry built from the two modules that define nodes; nothing is imported, evaluated or constructed by name from the document. An unknown tag is refused naming it, which is what lets a reader of a newer document say so rather than guess.

NOT_FINITE = {'inf': math.inf, '-inf': -math.inf, 'nan': math.nan} module-attribute #

TAG = '$' module-attribute #

WIRE_VERSION = 0 module-attribute #

from_json(text) #

The program a document holds.

Nothing is imported or evaluated: a tag names a class in a closed registry or the document is refused.

PARAMETER DESCRIPTION
text

What :func:to_json wrote.

TYPE: str

RETURNS DESCRIPTION
Program

The same :class:~math_spec.program.Program that was written.

RAISES DESCRIPTION
LanguageError

The document is not one this release can read — a wire version it does not know, or a tag naming no node.

Source code in src/math_spec/serialisation.py
def from_json(text: str) -> _program.Program:
    """The program a document holds.

    Nothing is imported or evaluated: a tag names a class in a closed registry
    or the document is refused.

    Args:
        text: What :func:`to_json` wrote.

    Returns:
        The same :class:`~math_spec.program.Program` that was written.

    Raises:
        LanguageError: The document is not one this release can read — a wire
            version it does not know, or a tag naming no node.
    """
    document = json.loads(text)
    if (version := document.get('version')) != WIRE_VERSION:
        raise LanguageError(
            f'this is a program document at wire version {version!r}, and this release reads '
            f'{WIRE_VERSION}. The wire version moves when the encoding changes, so a document '
            f'from a newer release is not one to guess at — read it with the release that wrote it.'
        )
    return _decode(document['program'])

to_json(model, *, indent=None) #

A model as the JSON a consumer in any language reads.

PARAMETER DESCRIPTION
model

Whatever every other verb takes — a YAML path, a mapping, a loaded :class:~math_spec.model.Spec, or a :class:~math_spec.program.Program already.

TYPE: str | Path | dict[str, Any] | Spec | Program

indent

Passed to :func:json.dumps. None is the compact form; an integer is what a document meant to be read or diffed wants.

TYPE: int | None DEFAULT: None

RETURNS DESCRIPTION
str

Strict JSON — no Infinity, no NaN, nothing a conforming reader

str

in another language refuses.

RAISES DESCRIPTION
LanguageError

The model is not one, named with its rewrite.

Source code in src/math_spec/serialisation.py
def to_json(model: str | Path | dict[str, Any] | Spec | _program.Program, *, indent: int | None = None) -> str:
    """A model as the JSON a consumer in any language reads.

    Args:
        model: Whatever every other verb takes — a YAML path, a mapping, a
            loaded :class:`~math_spec.model.Spec`, or a
            :class:`~math_spec.program.Program` already.
        indent: Passed to :func:`json.dumps`. ``None`` is the compact form;
            an integer is what a document meant to be read or diffed wants.

    Returns:
        Strict JSON — no ``Infinity``, no ``NaN``, nothing a conforming reader
        in another language refuses.

    Raises:
        LanguageError: The model is not one, named with its rewrite.
    """
    document = {'version': WIRE_VERSION, 'program': _encode(to_program(model))}
    return json.dumps(document, indent=indent, allow_nan=False)