src/ail_ref/canonical.py

Read-only source from the unchanged 0.3.4 kit. Existing license terms apply.

from __future__ import annotations

import json

from .ast import Alias, Atom, Boolean, Call, Document, ListValue, Number, Record, String, Unknown, Value, Variable


MESSAGE_FIELD_ORDER = {
    name: index
    for index, name in enumerate(
        ["id", "act", "ref", "from", "to", "registry", "body", "target", "conf", "evidence", "provenance", "constraints"]
    )
}


def _number_text(number: Number) -> str:
    value = number.value
    if value == value.to_integral():
        return str(value.quantize(1))
    result = format(value.normalize(), "f")
    if result.startswith("."):
        result = "0" + result
    if result.startswith("-."):
        result = "-0" + result[1:]
    return result


def _field_sort_key(record_name: str, item: tuple[str, Value]) -> tuple[int, str]:
    key = item[0]
    if record_name == "M":
        return MESSAGE_FIELD_ORDER.get(key, 1000), key
    return 1000, key


def canonical_value(value: Value) -> str:
    if isinstance(value, Atom):
        return value.value
    if isinstance(value, Alias):
        return value.target.value
    if isinstance(value, Variable):
        return f"${value.name}"
    if isinstance(value, Unknown):
        return "_"
    if isinstance(value, String):
        return json.dumps(value.value, ensure_ascii=False, separators=(",", ":"))
    if isinstance(value, Number):
        return _number_text(value)
    if isinstance(value, Boolean):
        return "TRUE" if value.value else "FALSE"
    if isinstance(value, ListValue):
        return "[" + ",".join(canonical_value(item) for item in value.items) + "]"
    if isinstance(value, Call):
        return value.name + "(" + ",".join(canonical_value(arg) for arg in value.args) + ")"
    if isinstance(value, Record):
        fields = sorted(value.fields, key=lambda item: _field_sort_key(value.name, item))
        return value.name + "{" + "".join(f"{key}={canonical_value(item)};" for key, item in fields) + "}"
    raise TypeError(f"unsupported value {type(value)!r}")


def canonicalize(document: Document) -> str:
    return "\n".join(f"{unit.header}\n{canonical_value(unit.value)}" for unit in document.units)


def pretty_value(value: Value, indent: int = 0) -> str:
    pad = "  " * indent
    if isinstance(value, Record):
        fields = sorted(value.fields, key=lambda item: _field_sort_key(value.name, item))
        lines = [f"{value.name}{{"]
        for key, item in fields:
            rendered = pretty_value(item, indent + 1)
            if "\n" in rendered:
                first, *rest = rendered.splitlines()
                lines.append(f"{'  ' * (indent + 1)}{key}={first}")
                lines.extend(rest)
                lines[-1] += ";"
            else:
                lines.append(f"{'  ' * (indent + 1)}{key}={rendered};")
        lines.append(pad + "}")
        return "\n".join(lines)
    if isinstance(value, Call) and value.args:
        simple = canonical_value(value)
        if len(simple) <= 88:
            return simple
        lines = [value.name + "("]
        for index, arg in enumerate(value.args):
            suffix = "," if index < len(value.args) - 1 else ""
            lines.append("  " * (indent + 1) + pretty_value(arg, indent + 1) + suffix)
        lines.append(pad + ")")
        return "\n".join(lines)
    return canonical_value(value)


def pretty(document: Document) -> str:
    return "\n\n".join(f"{unit.header}\n{pretty_value(unit.value)}" for unit in document.units)