Skip to content

astero.coverage

What a dispatch table covers of a grammar, and what it misses.

astero.coverage

What a consumer's dispatch table covers of a grammar, and what it does not.

A singledispatch registry, a match over node types, a dictionary of handlers: each is an enumeration of the language sitting next to another enumeration of the language. They have to agree, and when they drift the symptom is a NotImplementedError in someone's program rather than a failure in a test.

Comparing the two is four lines, which is why the first two consumers each wrote their own copy and the copies had already diverged. That is the shape this project exists to remove, so it lives here now.

cover = dispatch(PY, (compile_expr.registry, compile_stmt.registry),
                 bases=("stmt", "expr"),
                 accounted={"inline": INLINE, "unimplemented": TODO})
assert not cover.missing, cover.explain()

What accounted is for: a production may legitimately have no handler of its own. It may be eliminated by an earlier pass, emitted by whatever contains it, or simply not implemented yet. Each of those is a decision, so each is named, and absent catches an entry that no longer corresponds to anything.

Coverage dataclass

A dispatch table measured against a grammar.

missing property

missing: frozenset[str]

Productions that can appear, are not handled, and have no reason.

absent property

absent: frozenset[str]

Excused names this grammar has no production for.

Usually a stale entry. Sometimes a production the running interpreter does not have, since a grammar read off ast moves with the version, so a consumer subtracts the ones it knows are version-dependent.

redundant property

redundant: frozenset[str]

Excused names that are handled after all, so the excuse is stale.

handlers

handlers(
    registries: Iterable[Mapping[type, Any]],
) -> frozenset[str]

Production names a set of singledispatch registries handles.

object is the fallback every registry carries and never a production.

visitors

visitors(
    *classes: type, prefix: str = "visit_"
) -> frozenset[str]

Production names the visit_<Production> methods of classes handle.

The third way a consumer enumerates a language, after a singledispatch registry and a match. ast.NodeVisitor dispatches on visit_ + type(node).__name__, so the method names are the table.

Read from each class's own __dict__ along the MRO, framework bases excluded. dir would be shorter and wrong: ast.NodeVisitor defines visit_Constant, so every subclass would report handling Constant, and a gate built on that certifies a production nobody wrote. A class that defines visit_Constant itself still counts, because the lookup is by where the method is defined.

generic_visit is the fallback rather than a production, and it does not carry the prefix.

Pass several classes when a consumer splits its dispatch, the way an expression visitor and a statement visitor divide the grammar::

cover = Coverage(
    handled=visitors(ExpressionCodegen, FunctionCodegen),
    expected=PY.concrete("expr") | PY.concrete("stmt"),
)

match_arms

match_arms(*functions: Any) -> frozenset[str]

Production names the match statements in functions have a case for.

A match over node types is the second way a consumer enumerates a language, and the one handlers cannot read: there is no registry to inspect, only case Cls(...) patterns in the source. This reads them back from it.

Top-level patterns only, and both sides of an alternative: case ArrayDim() | ArrayStride() handles two. A nested pattern is part of the arm's condition rather than the thing it dispatches on, so case Const(value=float()) handles Const and not float. A case _ handles nothing by name, which is the point: a fallback is not coverage.

Raises OSError if a function's source is unavailable, which is the honest answer for something defined in a REPL or a C extension.

dispatch

dispatch(
    grammar: Grammar,
    registries: Iterable[Mapping[type, Any]],
    *,
    bases: Iterable[str] = (),
    accounted: Mapping[str, Iterable[str]] | None = None,
) -> Coverage

Measure registries against the productions of grammar.

bases narrows the obligation to what can appear under those productions, which is how a statement dispatcher is not asked about expressions. With no bases the whole grammar is the obligation.