Skip to content

Roles

Roles are what ASDL does not have and what every derivation needs.

role meaning
child structural containment; drives traversal
attr plain data, never traversed
def(ns) introduces a name in namespace ns
use(ns) refers to a name in ns
defuse(ns) both, for read-modify-write positions
del(ns) removes a name
declare(ns) global and nonlocal: names a binding that lives elsewhere

A role belongs to the parent's field

Not to the child node. This is the decision that makes ctx derivable.

In Python's AST a Name carries ctx=Store() or ctx=Load(), and every hand-written rewrite has to set it. But ctx describes the position the Name occupies, so the position owns it: Assign.targets is def(vars), BinOp.left is use(vars), and ctx leaves the grammar entirely and becomes derived.

The consequence is that a rewrite rule cannot mention a context, so it cannot get one wrong. b[1] += 5 and x += 5 go through the same rule.

Namespaces separate what looks alike

Two fields can hold the same type and mean different things.

An SSA value and a mutable stack slot are both Value-typed. def(vals) and def(slots) keep a pass over values from ever reaching the slot.

In Python, Attribute.attr and Name.id are both identifiers. Without a namespace, ident_slots() reports both, and a renamer built on it rewrites x.foo into a different attribute. ident_slots(VARS) asks about variables.

When two questions are close enough to confuse, make the grammar answer both and name them differently. That is the second failure mode this design guards against, and it is quieter than drift because nothing disagrees with anything.

A field may carry a condition

"target": ((defines(VALS), Present("declare")),
           (uses(VALS),    Absent("declare"))),

One slot, two roles, told apart by a sibling field: a declaring assignment defines its target, a re-assignment reads it. Field.applies(node) answers for a given node, and reads/binds apply it for you.

The condition language is two words. Admitting an arbitrary predicate would readmit host code into the declaration, which the design exists to remove.

What roles do not say

Roles are structural. They do not carry types, effects, costs, or whether an operand is provably a constant. Those are attributes of a program, and the guard vocabulary in emit_rules stops at the same line for the same reason.