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
¶
Productions that can appear, are not handled, and have no reason.
absent
property
¶
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
¶
Excused names that are handled after all, so the excuse is stale.
handlers ¶
Production names a set of singledispatch registries handles.
object is the fallback every registry carries and never a production.
visitors ¶
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 ¶
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.