Skip to content

astero.tables

Operation-by-type matrices, which no grammar derives.

astero.tables

Families: the tables a grammar cannot derive, declared once.

astero.grammar handles structure the grammar determines. This handles the other kind. Which runtime symbol implements list-append for Str is a fact about the runtime, so nothing derives it and it has to be authored.

What goes wrong is not that it is authored. It is that a family of related operations gets written as N separate dictionaries, each repeating the index, each free to disagree about the shared rules, and each with its own way of saying "this combination does not exist". A Family is one declaration:

LIST = Family.parse(
    "list",
    normalize={"Bool": "Int64"},
    matrix='''
                      Int64            Float64          Str
        append        LIST_APPEND_I64  LIST_APPEND_F64  LIST_APPEND_STR
        get           LIST_GET_I64     LIST_GET_F64     LIST_GET_STR
        get_unchecked LIST_GET_I64_U   LIST_GET_F64_U   .
    ''',
)

Three things follow. The normalization is stated once and applies to every row, so a rule like "a Bool crosses as an Int64" cannot be spelled one way here and another way there. A hole is written as a hole, rather than as a missing key plus a .get() that returns None. And totality is checkable: every row covers the index, or says explicitly where it does not.

TableError

Bases: ValueError

A family that does not describe a rectangle.

Family dataclass

A set of operations over one index, with one normalization.

keys

keys() -> tuple[str, ...]

Every key a lookup accepts, including the normalized aliases.

resolve

resolve(key: str) -> str

Apply the normalization. Bool is an Int64 on the wire.

lookup

lookup(op: str, key: str) -> str | None

The symbol for op at key, or None where the family has a hole.

column

column(key: str) -> dict[str, str | None]

Every operation at one key.

as_dict

as_dict(
    op: str, *, include_aliases: bool = True
) -> dict[str, str]

One row as the plain dictionary a hand-written table would be.

Holes are absent, which is what a dictionary can express. This is the shape a consumer that has not adopted families still expects.

holes

holes() -> tuple[tuple[str, str], ...]

Every declared hole, as (operation, key).

check

check() -> list[str]

Complaints about the family's shape. Empty when it is a rectangle.

as_matrix

as_matrix() -> str

The declaration, rendered back. Holes show as ..

parse classmethod

parse(
    name: str,
    matrix: str,
    normalize: Mapping[str, str] | None = None,
) -> Family

Read a whitespace-aligned matrix. First non-blank line is the index.