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)