astero.emit_rules¶
A code generator written as guarded templates over the grammar.
astero.emit_rules ¶
Emission as a declaration: one rule per production, guarded.
All three compilers in the corpus emit a language, not machine code, so
there is no register allocation, no scheduling and no cost model. What is left
is producing text with the children in the right places, bracketed and laid
out, and that is written today as match, isinstance and f-strings, once
per target.
A rule is a production name, a template, and an optional guard:
JS.rule("BinOp", "{left} {op} {right}", when=Both(NUMERIC))
JS.rule("BinOp", "_pyfunc_op_{op:name}({left}, {right})")
Rules are tried in order and the first whose guard holds wins, as in
astero.python.rewriting. A rule with no guard always holds, so it goes last and
gives the production an answer; a production with no answer at all is a hole
the coverage check reports rather than a NotImplementedError in someone's
program.
{field} emits that child and brackets it if the precedence table says
so, which is the part the grammar and astero.emit already know. {field:x}
is a projection when x is a declared projection name and a separator
otherwise, so {args:, } joins a sequence and {result:type} runs the
target's type spelling. {field?} omits an absent optional field.
A template cannot call, branch or hold state. Everything conditional is a
guard from the closed vocabulary below, and everything computed is a
projection declared once by name. That restriction is the same one
Present/Absent puts on the grammar, and Parr's on StringTemplate: a
template that can compute stops being a specification of output.
Is
dataclass
¶
The inferred type of field is one of types.
Both
dataclass
¶
Both operands of a binary production have one of types.
OpIs
dataclass
¶
The production's operator field is one of ops.
Const
dataclass
¶
field is a literal, optionally of a given Python type.
Has
dataclass
¶
The field is present and not empty.
The same question Present asks in the grammar, and for the same reason:
yield with a value and yield without are two spellings, not one
spelling with an optional hole, because the second has no trailing space.
All
dataclass
¶
Every guard holds. Any is the other connective.
Either
dataclass
¶
At least one guard holds.
Not
dataclass
¶
The guard does not hold.
A connective rather than a new predicate, which matters: the atoms stay
four. can_use_strict_equality is expressible without it, as a
disjunction of two Boths, and unreadably so.
EmitError ¶
Bases: Exception
No rule applied, or a template named something the grammar lacks.
Emitter
dataclass
¶
A target's emission rules, over one grammar.
declared_level ¶
declared_level(node: Any) -> Level | None
The binding power the table gives node, or None if it gives none.
Undeclared is not a level, and the two questions below answer it differently, so the lookup is separate from both.
level_of ¶
level_of(node: Any) -> Level
How tightly node binds as emitted.
Which rule fires decides this, not the node alone: a + b spelled
a + b binds at the + level, and spelled _pyfunc_op_add(a, b) it
is an atom. Reading the node alone bracketed the call.
Undeclared means ATOM here: a production the table omits is emitted
as a primary, so nothing brackets it.
inner_level ¶
inner_level(node: Any) -> Level
How tightly the operator inside a template binds.
Not the same question as level_of, and undeclared means the opposite
thing: a production the table omits imposes nothing on its holes. A
statement is the case that matters, and it is most of a target. t = 0
came out t = (0) and return n * 2 came out return (n * 2),
because Assign and Return are in no precedence table, and the
missing entry was read as ATOM, the tightest context there is,
rather than as no context at all.
// emitted as Math.floor({left}/{right}) is why this is not simply
level_of: the result is an atom and the holes sit either side of a
/, so bracketing them against the atom gave Math.floor((hi - lo)/(2)).
emit ¶
The text for node, bracketed for the context it sits in.
source_map ¶
(start, end, (line, column)) for every emitted node with a position.
start and end index to_text(node), and the pair is where in the
source that text came from. A source map is this list in whatever
encoding the target wants.
Every rule's output carries the position of the node it emitted, so this needs no cooperation from the rules and no extra hole. A node the host built rather than parsed has no position and contributes nothing, which is why the list is usually shorter than the tree.
label ¶
A label unique to node, stable for the same name.
A statement that branches needs somewhere to jump to, and the same
label appears twice: once where it is defined and once where it is
named. `{&done}` gives both the same text, and a second `While` in the
same function gets a different one.
This is what a flat target was missing. Statement *structure* already
worked — a body hole renders the list, and a newline separator joins
it — so `while` on a stack machine is a template like any other:
"{&top}:
{test} JZ {&done} {body: } JMP {&top} {&done}:"
to_lines ¶
The emitted lines, split where the templates said to.
to_text(node).splitlines() is the obvious thing and it is wrong as
soon as a value contains a newline: a string literal splits into two
instructions. This walks the document, so only a break a template
wrote is a break.