IR¶
ppy_compiler.ir is the typed canonical IR that sits between analysis and
the backends. Its modules:
model: the data structuretypes: the typesdialect: the extension pointverify: the rulesprinter,parser,codec: the text form
The IR describes the format and the dialects.
Model¶
ppy_compiler.ir.model
¶
The IR's data model: modules, functions, blocks, operations, values.
Typed SSA with an explicit control-flow graph. Every value is defined once -- as a block argument or as the result of one operation -- and carries one type; every block ends in one terminator; branches pass arguments to the blocks they reach. The structure is plain objects with use lists, so a rewrite can replace a value everywhere it is read and a verifier can walk what it wants.
Nothing here knows what an operation means. An operation is a name in a
dialect, operands, results, attributes, successors, and nested regions; the
dialect's OpSpec says what is allowed, and the verifier asks it.
SymbolRef(name)
dataclass
¶
A reference to a symbol by name: the @callee of a call.
Symbol(name, visibility='public')
dataclass
¶
A name in the module's symbol table and who may see it.
Value(type_, name=None)
¶
Something an operation reads: a block argument or an operation result.
replace_all_uses_with(other)
¶
Every read of this value reads other from now on.
Successor(block, arguments=())
¶
A branch target with the arguments it receives.
Region(parent=None)
¶
A list of blocks; the first is the entry.
function
property
¶
The function this region belongs to, however deeply it nests.
Block(name, region=None)
¶
Operation(name, operands=(), result_types=(), attributes=None, successors=(), location=None, result_names=())
¶
One operation: results = dialect.name operands successors attrs regions.
erase()
¶
Remove this operation from its block and drop every use it holds.
Global(symbol, type, value=None, constant=True, location=None)
dataclass
¶
A module-level constant or variable.
IRFunction(symbol, params, results, attributes=None, location=None)
¶
IRModule(name, dialects=None)
¶
require(dialect, version)
¶
Record that the module uses dialect at version.
Builder(block=None, location=None)
¶
Appends operations at an insertion point.
The dialect modules put their typed constructors on top of create:
core.add(builder, x, y, overflow="python").
Types¶
ppy_compiler.ir.types
¶
The types a value in the IR can have, and their text.
Every value has exactly one type. The core types are the ones every backend
must know -- machine scalars, pointers, buffers, vectors, tuples, structs,
and futures -- and a dialect adds its own through DialectType, which the
core parses generically as name<args> and the dialect verifies.
A type is an immutable value that compares by content and prints to one spelling, so the same program always prints to the same bytes.
TypeError_
¶
Bases: ValueError
A type that cannot be spelled or parsed.
IRType()
dataclass
¶
Base of every IR type. Subclasses are frozen dataclasses.
BFloat16Type()
dataclass
¶
Bases: IRType
Brain floating point: 8 exponent bits and 7 explicit fraction bits.
This is distinct from IEEE binary16 and deliberately not a FloatType, so a backend dispatching IEEE types by width cannot substitute f16.
PtrType(pointee, address_space='generic', mutable=True)
dataclass
¶
Bases: IRType
ptr<T, space, mut|const>: a pointer to T in an address space.
stack memory comes from core.alloca and may not escape the function;
generic is anything the host addresses. A dialect adds spaces of its
own (global, shared, ...).
BufferType(element)
dataclass
¶
StructType(name, fields)
dataclass
¶
DialectType(dialect, name, args=())
dataclass
¶
Bases: IRType
dialect.name<args>: a type a dialect owns.
The core keeps the arguments as parsed -- types, integers, or bare words -- and the dialect's verifier gives them meaning.
is_arithmetic(t)
¶
Something core.add and friends operate on: a number, or a vector of them.
scalar_of(t)
¶
The element of a vector, or the type itself.
parse_type(text)
¶
The type text spells; raises TypeError_ on anything else.
Dialects¶
ppy_compiler.ir.dialect
¶
Dialects: the units the IR is extended in.
A dialect owns a namespace of operation names and types, and says what each
operation requires -- how many operands and results, which attributes, what
the types must be -- through an OpSpec the verifier consults. The core
dialect is one of them; every other is registered the same way, by the
compiler or by a plugin, and a module records which dialects it uses at
which version so that a reader without one refuses it instead of guessing.
OpSpec(name, terminator=False, pure=False, commutative=False, variant_attribute=None, variants=(), inline_attribute=None, verify=None, operands=None, results=None, required_attributes=(), successors=None, regions=0)
dataclass
¶
What one operation name requires, and what it may be treated as.
Dialect
¶
A namespace of types and operations; subclasses register theirs.
register_types(registry)
¶
Types this dialect owns are verified by verify_type; nothing to do by default.
register_operations(registry)
¶
Add this dialect's OpSpecs: registry.add_op(spec).
register_patterns(registry)
¶
Canonicalization patterns (ir.pattern); nothing by default.
register_lowerings(registry)
¶
Lowerings to other dialects or to a backend; nothing by default.
verify_type(t)
¶
Why t is not a type this dialect defines, or None when it is.
address_spaces()
¶
Address spaces this dialect gives pointers.
verify_function(function, checker)
¶
Rules over a whole function -- its attributes, what its body may hold.
The verifier asks every registered dialect after a function's own checks; the default has nothing to say.
DialectRegistry()
¶
registry()
¶
The process-wide registry, with the builtin dialects registered.
Passes¶
ppy_compiler.ir.passes
¶
The pass manager: passes over modules, in order, with what they need.
A pass names the analyses it requires, preserves, and invalidates, and the context serves analyses from a cache that a pass's own declaration empties. Between passes the manager can verify the module, so that a pass which breaks it is named in the error rather than found by the next one. Plugins add passes at named stages; the manager runs them where the stage falls in the pipeline.
PassContext(registry=None, *, verify_after_each=False, options=None)
¶
What passes share: the registry, options, remarks, and cached analyses.
Pass
¶
One transformation of a module.
run(module, ctx)
¶
Transform module; return whether anything changed.
PassReport(entries=list())
dataclass
¶
What ran, whether it changed anything, and how long it took.
PassVerificationError(pass_name, errors)
¶
Bases: Exception
A pass left the module invalid; the pass is named.
Patterns¶
ppy_compiler.ir.pattern
¶
Rewriting the IR by patterns, to a fixed point.
A pattern looks at one operation and either rewrites it through the rewriter -- which is the only way anything is changed, so every change is recorded and every affected operation is looked at again -- or says why it does not apply. The greedy driver applies a set of patterns until none applies, with a cap that turns a pattern that never settles into an error naming it rather than a compiler that never finishes.
Verification¶
ppy_compiler.ir.verify
¶
The verifier: what every module must satisfy before anything reads it.
Structure first -- every block ends in one terminator, branches reach blocks
of the same region with the arguments those blocks take, every value is
defined before it is used along every path, every symbol a call names
exists -- then the dialects' own rules, operation by operation. A failure
is a list of errors with the operation each sits on, never an exception
out of the middle of a walk; verify_or_raise turns the list into one
exception for callers that want that.
The linker¶
ppy_compiler.ir.linker
¶
The IR linker: one program from a project's module IRs (spec 10, 11).
Every module lowers on its own to an IRModule whose calls into another
module go through a declaration -- a function with no body, carrying the
callee's symbol. Linking puts the modules together: a definition answers
the declarations of its symbol, which go away; two definitions of one
symbol are one when both are the same generic instance (a specialization
each module made for itself) and an error otherwise; a private symbol --
a string constant, a helper -- that two modules both spell is renamed after
its module, and every reference follows. The dialects a module needs the
program needs, at the newest version any asked for; the libraries a module
links the program links. What is left unresolved is reported by name, so
the driver can keep its callers on CPython the way the frontend keeps a
caller of a function that did not lower.
The linked module is what whole-program optimization runs on and what
ppy emit ir --linked prints; a backend sees one module either way.
Text¶
ppy_compiler.ir.codec
¶
.ppyir: the IR on disk, versioned.
The text the printer writes is the format: a header naming the schema version and every dialect the module uses with its version, then the module. Reading checks the header before anything else -- a newer schema, an unknown dialect, or a dialect at a version this compiler does not have is refused with the reason, not parsed on the hope that it is close enough. Encoding is deterministic: the same module encodes to the same bytes, whatever built it.
The format is experimental in 0.2.0: a reader may refuse what an older writer wrote, and says so.
ppy_compiler.ir.printer
¶
The IR as text, one spelling per module.
Values are named in definition order: a value keeps the name it was given when no earlier value in the function took it, and is numbered otherwise. Attributes print sorted by key. So two modules with the same content print to the same bytes, and a module printed after being parsed prints the same text again.
ppy_compiler.ir.parser
¶
Reading the text the printer writes.
One cursor over the source; types are parsed by the type grammar's own parser at that cursor, so there is one spelling of a type. Every value is resolved when the function ends, so a branch may name a block or a value that is defined later in the text; a name defined twice or never is an error with its line.
parse_module(text, registry=None)
¶
The module text spells; the header is validated by the codec.
Shared passes¶
ppy_compiler.ir.transforms
¶
Passes over the IR that every pipeline shares.
AutodiffError
¶
Bases: ValueError
A function the transform cannot differentiate, with the reason.
FuseTensor()
¶
Bases: FunctionPass
TensorCanonicalize
¶
AsyncLoweringError
¶
Bases: ValueError
A coroutine the state machine cannot hold: a buffer, a vector.
LowerParallel(backend='threads', threads=1, minimum=4096)
¶
LoweringError
¶
Bases: ValueError
Tensor IR the lowering cannot make memory of, with the reason.
LowerTensor(lapack=True)
¶
Bases: Pass
Rewrite every tensor, linalg, fft, sparse, columnar, and arrow operation as core loops.
lapack says whether the factorizations may call LAPACK; without it
they are refused with the reason.
AnnotateProfile(profile)
¶
FunctionProfile(symbol='', cfg='', calls=0, blocks=dict(), branches=dict(), arguments=dict(), generic='')
dataclass
¶
What one function did: its calls, its blocks, its branches, its arguments.
merge(other)
¶
Add other's counts, when it measured the same graph; take it whole otherwise.
Instrument
¶
Profile(functions=dict(), runs=0, program='', compiler='', path='')
dataclass
¶
A .ppyprof: every profiled function by qualified name, over one or more runs.
ProfileError
¶
Bases: Exception
A .ppyprof that is missing, unreadable, or not a profile.
Sanitize(kinds)
¶
SimplifyCFG
¶
Bases: FunctionPass
Inline(budget=INLINE_BUDGET)
¶
differentiate(module, function, *, wrt=(0,), value=False, name=None)
¶
The gradient of function with respect to the parameters at wrt.
A scalar parameter's gradient is a result, in wrt order after the
value when value is asked for; a buffer parameter's gradient is
written into a buffer parameter added after the function's own, one
per buffer in wrt, named grad_<parameter>.
canonicalization_patterns(registry)
¶
What every registered dialect contributes.
cfg_digest(function, registry=None)
¶
The shape a profile keys on: every block's label, terminator, and successors.
sanitizer_kinds(spelled)
¶
The kinds --sanitize a,b or a configured list names; a refusal names why.
internalize(module, keep)
¶
Every definition not in keep and not handed out by address becomes private.
default_pipeline(level=1, ctx=None, parallel=None, sanitize=(), until=None, instrument=False, profile=None, lower_tensors=True)
¶
The passes a module goes through before a backend, by optimization level.
parallel is the LowerParallel pass for the build's configuration;
it runs after the optimizations and the result is cleaned up again.
sanitize names the sanitizers a build asked for; their checks go in
before the optimizations, so what the optimizer removes they still
hold. until stops the pipeline at a point -- none before any pass,
after-canonicalization, after-fusion -- for ppy inspect --stage.
instrument places the profile counters, profile applies a recorded
profile; both work right after canonicalization, so a profile keys on
the graph a build sees at the same point.