Skip to content

astero.emit

The document tree, the renderer, and bracketing from a precedence table.

astero.emit

Emission: documents, and parenthesization derived from declared precedence.

Every compiler in the corpus builds target text by concatenating strings, and every one of them hand-rolls the question of when a subexpression needs brackets. That question has a known answer given a precedence table, so the table is declared and the brackets are derived.

Two pieces.

A document is a tree, not a string: text, concatenation, nesting, and a line break that a group may or may not take. Layout becomes the renderer's job, and a span rides on a document, so a source map is a projection of the emitted document rather than a structure threaded alongside the emitter.

A precedence table gives each production a binding power and an associativity. A child is bracketed when its power is lower than the position it sits in, which is the whole rule. Nothing is written per production.

Doc dataclass

A layout-independent rendering, with an optional source span.

Nest dataclass

Bases: Doc

Indent everything inside by indent columns after a line break.

Line dataclass

Bases: Doc

A hard break, followed by the indentation in force where it appears.

In Python indentation is semantic, so a break in a statement body is a correctness question rather than a cosmetic one. A soft break that a group may flatten to fit a width is a separate concept and is not needed yet.

Level dataclass

A binding power and how it associates.

render

render(doc: Doc, indent: int = 0) -> str

Text for a document. A Line breaks and re-indents to its Nest depth.

spans

spans(
    doc: Doc, offset: int = 0, indent: int = 0
) -> Iterator[tuple[int, int, tuple[int, int]]]

(start, end, span) for every document carrying one.

A source map is this list, and start and end index render(doc).

indent is not decoration. A Line renders as a break plus the indentation in force, so everything after one inside a Nest is wider than the same document rendered at column zero. Measuring the parts at zero put every offset after the first break too far left, which a caller would have seen as a source map that drifts down the file. The emitter threads no source map, so nothing here noticed.

joined_by

joined_by(sep: Doc, parts: list[Doc]) -> Doc

Like joined, with a document separator rather than a string.

needs_parens

needs_parens(
    inner: Level, outer: Level, *, on_right: bool
) -> bool

Whether a subexpression at inner needs brackets inside outer.

The whole parenthesization rule, in three lines, given a table. A child binding less tightly always needs them; a child binding equally tightly needs them on the side the operator does not associate towards.