TileFoundry Spec — analysis (authored-HIR metrics + per-stage target facts)¶
This spec owns TileFoundry's fact layer: everything a later stage decides over, and nothing that decides anything itself. It has two surfaces:
| Surface | Entry | What it states |
|---|---|---|
| Program check | check_program(module, function, level=..., budget=..., analyzers=...) |
an inlined Function view after validating one authored program, its declared topology, and what each requested analysis needs of it |
| Composed measurement | analyze(module, function, analysis=...) |
one or more root analyses and their union dependency closure, leaving typed Metadata on the IR |
Per-Op semantic derivation — typeinfer, the forward access relation, shard propagation — is owned by semantic-analysis, and the registries behind it by visitor-registry; the families below consume the forward relation (visitor-registry §4.1) rather than restating it.
1. Authored-HIR metrics¶
The measurement entry is the composed operation (§2). Each family below owns its record, field derivation, target facts, and rendered forms. The command line composes one call per requested family and renders those results together (cli §Analyze).
Typed records use the immutable IRMetadata and optional comment interface
defined by core-ir §2. Their attachment point says what
they describe: a record on a Call describes that call, while a record on a
Function describes the whole function.
- constraints:
- One record type MUST mean the same quantity at every attachment point.
- A
Function-attached record MUST NOT be read as data the Function inherently carries. It states what one analysis found for one invocation, and there MUST be no cross-call cache behind it.
1.2 Analysis families¶
The first families are compute-cost, memory, roofline, and performance.
Each owns its record types and declares its dependencies and output additions.
| Selector | Requires | Owns | Attaches to | Rests on | Text summary adds | Annotates equations |
|---|---|---|---|---|---|---|
compute-cost |
- | ComputeCostMetadata |
every measured Call and the Function | the authored program | compute-cost |
every measured Call |
memory |
- | MemoryMetadata, TrafficMetadata, LoopFootprintMetadata |
MemoryMetadata on the Function; TrafficMetadata on every measured Call and the Function; LoopFootprintMetadata on every GridRegionExpr |
the authored program, MemoryHierarchyFacts |
peak-footprint, traffic, advisory |
none |
roofline |
compute-cost, memory |
RooflineMetadata |
every measured Call and the Function | ThroughputFacts |
roofline |
every measured Call |
performance |
compute-cost, memory |
PerformanceMetadata, PerformanceSummaryMetadata |
PerformanceMetadata on every Call with a modeled duration; PerformanceSummaryMetadata on the Function |
ThroughputFacts, ParallelCapacityFacts, MemoryHierarchyFacts |
performance |
every Call with a modeled duration |
Every compact text summary begins with these two lines:
# example
# analysis target=<target> module=<module> function=<function> topology=<level>
# selection requested=<selector>[,<selector>...] executed=<selector>[,<selector>...]
Every summary line is one record walked exactly as an annotated equation is (inspection §2.8), so the two surfaces cannot spell one value two ways. What the report is about and what was asked of it are records of the report rather than of the IR; every other summary line is a record of the selected Function.
The JSON report carries the same identity and selection in target, module,
function, topology, requested, and executed. Whole-function
projections are under function_records; calls is a value-ordered list whose
entries have a value label and one key per selected family. loops is the
corresponding authored-loop list, labelled by induction variable. When memory is
selected, a loop whose backing storage has a same-scope implicit cache also has
cache-pressure: one target-aware row per cache, computed from the loop's
device-wide access footprint. totals appears when the selected view includes
compute cost or roofline's bounded work evidence.
One result is rendered once, and every surface reads that rendering:
def report(result: AnalysisResult) -> dict[str, object]:
"""Project one composed Analyze result into a shared rendering structure."""
...
def render_analysis(result: AnalysisResult) -> AnalysisRendering:
"""Render one annotated program and its report data in a single pass."""
...
reportMUST accept oneAnalysisResultand read every requested family's records from that result's record-bearing Function. It MUST NOT merge independently rebuilt Functions by identity, origin, dimensions, or walk position.- Text, JSON, and annotated HIR MUST come from one
render_analysiscall over the same result's Function and selected Metadata types. Each rendered Call record'svalueis<left-hand-side>:<line>, wherelineis the physical line containing that statement's=, even when its comment ends a later line of the same statement. Both surfaces MUST use the line locations collected by that one printer pass rather than recover them from names or text. - A Call equation carrying a record MUST state it in that line's Metadata comment. A carry update is a name rebinding rather than a Call equation, so it receives neither a comment nor a report row.
- A parameterized loop occurrence MUST stay one record. Neither surface expands it into one entry per trip.
- A compute-cost comment MUST state one unit's share beside the whole quantity it is a share of, and JSON MUST expose all four quantities without reconstructing any of them from the others.
A value renders by the type its field holds, and those forms and their separators are owned by inspection §2.8. What this layer settles is which type a field holds and what its keys name:
- A mapping's key is a dtype, a storage level, or an operand position -- an
argument integer, or
resultfor the value the Call produces. <resource>iscompute,memory,balanced,unrated, ornone.-
Bytes moved are
TrafficBytes; a whole quantity paired with one unit's share isTotalAndPerUnit; a Call's occurrence on the timeline is oneTripInterval. -
constraints:
- A family MUST obtain hardware only through a Facts aggregate it declares (target §11). Common analysis code MUST NOT branch on a concrete Target type, MUST NOT call a complete Target analyzer, and MUST NOT resolve an undeclared Target to a default.
- A family MUST read a dependency's record rather than recompute what it states. A number with two derivations has two answers.
- Before any member of a requested union closure writes Metadata, Analyze MUST
establish every requested root's family-specific readiness. Each family
states its own through the checker its descriptor carries, and one
metadata-free traversal of the derived program answers all of them.
Performance readiness requires a positive
ParallelCapacityFactsvalue for the selected topology, rates stated for that same level, and one valid execution placement for every occurrence that will take time. Where the buffers go is not a readiness question: nothing here decides it. Failing performance readiness MUST NOT make the same unplaced program invalid forcompute-cost,memory, orroofline. - Global logical work, per-unit work, and lifetime order MUST remain target-independent. Physical capacity, hierarchy relationships, and throughput comparisons are target-aware.
- A rendering MUST NOT be a field of the semantic result, and an analysis MUST NOT format one.
- A rendering MUST report what the caller requested. Dependency records nobody requested MUST stay on the IR and MUST NOT be reported except for roofline's bounded evidence defined below. Record ownership MUST come from the Target-selected descriptor (§2.2).
- Every rendering of one run MUST select records through one shared decision and MUST show only records actually written.
- Every reported quantity MUST come from a record, except a total that is the exact sum of records that state it. A quantity not derivable that way MUST be recorded by the analysis that computed it.
- Text and JSON MUST be built from one intermediate report and MUST carry the same conclusions.
- A family's JSON projection MUST be its record's fields under their own names,
with nothing left out: a default, a
null, and an empty mapping are each a fact a program branches on, and a key spelled by hand is a key that can drift from the field it reports. A field whose projection needs the expression the record is attached to MUST be declared as one, and MAY be absent where the program offers no such reading --operandson a Function Call, which charges a callee total no operand position names. A comment over the same record MAY state fewer keys, or projected ones (inspection §2.8), and what it leaves out MUST stay in the JSON projection. - A compact text summary MUST contain whole-function facts only. Per-value facts MUST stay on their annotated equations; JSON MAY retain operand names and types in its structured projection.
1.2.1 compute-cost¶
compute-cost measures the logical work of each authored Call without reading
target hardware facts. What an occurrence moves is the memory family's answer
(§1.2.2), read off the same registered evaluator.
class ComputeCostMetadata(IRMetadata):
"""One Call's logical work, as the authored program states it.
Attributes:
flops: attribute; Flop count per compute DType name, sorted by name.
flops_per_unit: attribute; Flop count performed by one unit of the analysed topology level.
service: attribute; Result count per service kind, sorted by kind.
service_per_unit: attribute; Result count per service kind for one unit of the analysed topology level.
"""
flops: tuple[tuple[str, int], ...] = ()
flops_per_unit: tuple[tuple[str, int], ...] = ()
service: tuple[tuple[str, int], ...] = ()
service_per_unit: tuple[tuple[str, int], ...] = ()
| Field | How it is computed | Reads the target |
|---|---|---|
flops |
For a primitive Call, run its registered cost evaluator over operand and result Types as written, then multiply by the enclosing recomputation factor and the number of positions in its execution scope. For a Function Call, take the callee's summed flops and multiply by the call site's factor. |
No |
service |
For a primitive Call, take its cost evaluator's service counts -- the results it asks a machine for that are not floating point -- and multiply by the same factor and execution-scope position count. A Function Call takes the callee's summed service. |
No |
service_per_unit |
The same evaluator over the same projected Types, multiplied by the same factor. A Function Call takes the equivalently projected callee total. | No; projection reads resolved Mesh and effective Module topology extents. |
flops_per_unit |
Use the same evaluator over Types projected through authored Splits at or coarser than the analysed level, then multiply by the same factor. A Function Call takes the equivalently projected callee total. |
No; projection reads resolved Mesh and effective Module topology extents. |
Requesting this family adds one summary line, prefixed by #: the Function's own
record, stated exactly as a Call's is. The whole program's work is not a second
record.
Every measured Call receives this annotation. Each key pairs the whole quantity
with one unit's share, so the two *_per_unit fields are not separate keys.
Each reported Call's JSON projection is under its compute-cost key:
{"flops": {<dtype>: <int>},
"flops_per_unit": {<dtype>: <int>},
"service": {<kind>: <int>},
"service_per_unit": {<kind>: <int>}}
- constraints:
- A record MUST be attached to every reachable
Calland to the Function. The Function record MUST include authored-loop repetition and therefore is not the direct sum of the one-occurrence Call records. - An op with no registered cost evaluator MUST raise
AnalysisError. - Missing program geometry MUST NOT be replaced with a target capacity.
- The enclosing recomputation factor MUST be the product of the authored loop trip counts for loops whose induction variable or carried argument the Call transitively reads. A loop-invariant Call MUST keep a factor of one. The same rule MUST apply to primitive and Function Calls.
- Downstream families MUST read the already-scaled record and MUST NOT apply authored loop trip counts a second time.
1.2.2 memory¶
memory measures whole-Function value lifetimes and footprints, decides where
each value's bytes live, and states what every occurrence moves and at which
level. The movement is read off the Op's own registered evaluator and the
amounts its access relations reach.
class TrafficMetadata(IRMetadata):
"""What one Call moves, or what one Function moves over all its trips.
Attributes:
whole: attribute; TrafficBytes per storage level name.
per_unit: attribute; TrafficBytes per storage level name for one unit of the analysed topology level.
operands: attribute; TrafficBytes per operand, positional against (*call.args, call); present only for a direct primitive call.
"""
whole: tuple[tuple[str, TrafficBytes], ...] = ()
per_unit: tuple[tuple[str, TrafficBytes], ...] = ()
operands: tuple[TrafficBytes, ...] = ()
class LevelFootprint:
"""How much of one memory level a function needs at its peak.
Attributes:
level: attribute; The memory level name.
peak_bytes: attribute; The largest simultaneous claim on the level.
persistent_bytes: attribute; The part that cannot be reclaimed.
capacity_bytes: attribute; The stated capacity, or None when unknown.
"""
level: str
peak_bytes: int
persistent_bytes: int
capacity_bytes: int | None = None
class BufferFootprint:
"""Bytes one authored loop touches in one buffer at one storage level.
Attributes:
buffer: attribute; The stable value name of the source buffer.
level: attribute; The storage level containing that buffer.
bytes: attribute; Deduplicated bytes reached by one logical position.
device_bytes: attribute; Deduplicated bytes in the union across positions.
repeated_bytes: attribute; Per-position bytes without deduplicating repeated access.
"""
buffer: str
level: str
bytes: int
device_bytes: int
repeated_bytes: int
class LoopFootprintMetadata(IRMetadata):
"""Known buffer accesses or a lower bound within one authored GridRegionExpr.
Attributes:
footprints: attribute; One row per source buffer and storage level.
known: attribute; Whether every access had a representable relation.
"""
footprints: tuple[BufferFootprint, ...]
known: bool
class ValueLifetime:
"""One value's residency, as positions in the function's value order.
Attributes:
binding: attribute; The parameter or authored binding name, unique in the function.
level: attribute; The memory level the value occupies.
bytes: attribute; Bytes the value occupies at that level.
defined_at: attribute; Position the value becomes resident.
last_used_at: attribute; Position it may be released.
persistent: attribute; Whether it is held for the whole function.
"""
binding: str
level: str
bytes: int
defined_at: int
last_used_at: int
persistent: bool = False
class AllocationMetadata:
"""What showing this function's buffers fit came to.
Attributes:
solver_status: attribute; `"optimal"` or `"feasible"`.
"""
solver_status: str
class MemoryMetadata(IRMetadata):
"""One function's memory behaviour against one target's hierarchy.
Attributes:
footprint: attribute; One row per level the function places values in.
lifetimes: attribute; One entry per value residency.
advisories: attribute; Capacity findings that do not invalidate the program.
allocation: attribute; What showing the addressable buffers fit came to.
"""
footprint: tuple[LevelFootprint, ...] = ()
lifetimes: tuple[ValueLifetime, ...] = ()
advisories: tuple[str, ...] = ()
allocation: AllocationMetadata | None = None
Every traffic amount here is what a boundary's own relation reaches. The Op's evaluator says which way each boundary moves and whether it materialises anything; it does not say how much, and an Op with no relation fails closed.
- constraints:
- One relation MUST answer for the whole program and for one unit, from one
registration; every boundary MUST be held to the iterations its participant
performs. Projecting an operand's Type is not enough, because a value nobody
sharded projects to the whole of itself: that is what makes a broadcast
operand cost its own size and a
Reshardthe distinct coordinates it reaches rather than a full source per participant. - Each leaf's bytes are charged at the level that leaf sits at. A
UMATleaf inCall.argscharges its own bytes atrmemand one appearing only in an Op attribute charges nothing, sowholeand anoperandsentry MUST NOT be assumed equal for a Type whose leaves occupy several levels: the first groups leaves by level, the second is one boundary's amount over all of them. Where those bytes were placed enters neither. - Two operands MAY name the same value; the
operandssplit MUST keep their positions distinct, and MUST omit an entry it cannot state rather than emit it empty.
Capacity is settled against the authored definition order, which fixes every buffer's lifetime before any of them is measured, so the only open question is whether the ones live at once fit together. An arrangement answers that question without being reported: no address or per-value buffer identity is a conclusion of this analysis.
- constraints:
- Capacity MUST be settled for the addressable levels
gmemandsmemonly, once per capacity domain that holds a buffer -- the whole target for a level owned target-wide, one per owning position otherwise -- with two buffers in one domain never live in the same bytes at once. Residency at another level MUST NOT make a program unplaceable, and a level owned per unit of a topology level other than the one being analysed MUST fail rather than be assumed. Domains holding the same buffers are one question, decided once. allocationMUST be absent only when no level could be projected against. A function with nothing addressable MUST record a settledallocation: the question was asked and there was nothing to decide. An attachedsolver_statusMUST be"optimal"or"feasible"; a domain that cannot fit, cannot be expressed, or does not settle in time MUST raiseAnalysisErrorsaying which of the three happened and leave no record. A domain that fits at once MUST be settled without searching, and one whose simultaneously live bytes exceed the capacity MUST be refused without searching.- A Call's
whole,per_unitandoperandsMUST state one occurrence. Only the Function record counts an occurrence as often as its authored loops repeat it, and itsoperandsMUST be empty: which operand moved what belongs to the occurrence, not to the total. - A capacity conclusion MUST NOT correct or invent a movement number. What an
occurrence moves is counted once from its own boundaries, so a function with
no
allocationstill carries traffic -- a different question from whether a time may be reported for it (§1.2.4) -- and a window whose start arrives at run time reads that start rather than becoming a full read of its source and a write of its result.
| Field | How it is computed | Reads the target |
|---|---|---|
BufferFootprint.buffer |
The label value lifetimes use for that value, from the same derivation. Grouping stays by buffer identity, because two structurally equal buffers are distinct allocations; the label never reads an address. | No |
BufferFootprint.level |
Read the source buffer's declared storage level. | No |
BufferFootprint.bytes |
Build relations from rank-preserving per-position Types, union the loop-prefixed access images, count the union's integer points, multiply by the dtype bit width, then round the whole buffer reading up to bytes. If the count is not an integer or exceeds repeated_bytes, that buffer reading is unavailable. |
No |
BufferFootprint.device_bytes |
Repeat the same exact union measurement from authored Types without shard narrowing, giving the union across logical positions in bytes. | No |
BufferFootprint.repeated_bytes |
Multiply each operand's per-position element count by its enclosing trip counts, sum accesses to the same buffer, multiply by dtype bit width, then round the whole buffer reading up to bytes. | No |
LoopFootprintMetadata.footprints |
One BufferFootprint per known source buffer and storage level, sorted by buffer then level. When known is false these rows are the available lower bound rather than an empty replacement. |
No |
LoopFootprintMetadata.known |
False when an access in the loop or a descendant loop lacks a representable forward relation, marking footprints as a lower bound; true otherwise. |
No |
ValueLifetime.binding |
Use the parameter or binding name, suffixed with : and the line of the value's source span when it has one. Repeated names already differ by the printer's numeric suffix in definition order; the line locates the row in authored source, which a suffix cannot. A value with neither name nor span is <value N> in definition order. |
No |
ValueLifetime.level |
Emit one lifetime per storage level occupied by the value's Type. | No |
ValueLifetime.bytes |
Project the Type through every authored split at or coarser than the explicit level's owner, then take its logical bytes; a target-owned or undeclared level remains global. |
MemoryHierarchyFacts.explicit_levels[].owner |
ValueLifetime.defined_at |
Position in the order of parameters followed by body Calls and Constants in SSA postorder. | No |
ValueLifetime.last_used_at |
Greatest recorded consumer position; the last position for a parameter, and also for the Function body when that body is itself a recorded value. | No |
ValueLifetime.persistent |
True for parameters and false for body allocations. | No |
LevelFootprint.level |
Each storage level with at least one lifetime, sorted by name. | No |
LevelFootprint.peak_bytes |
Largest sum of simultaneously live bytes at that level over the value order. | No |
LevelFootprint.persistent_bytes |
Sum of persistent lifetimes at that level. | No |
LevelFootprint.capacity_bytes |
Capacity of the matching explicit level, or None when it is unknown or undeclared. |
MemoryHierarchyFacts.explicit_levels[].capacity_bytes |
MemoryMetadata.footprint |
One LevelFootprint per occupied storage level. |
As above |
MemoryMetadata.lifetimes |
Every value residency except a Reshape or a Transpose, each of which describes bytes its operand already holds. |
As above |
MemoryMetadata.advisories |
Explicit peak overflow, cache/shared-capacity division, and same-scope authored-loop access-footprint findings. | MemoryHierarchyFacts |
TrafficMetadata.whole |
One occurrence's per-boundary movement asked of the Op's access relations in the whole program's window, charged to the storage levels its operand Types name and grouped by level. On a Function, summed over every reachable occurrence, each counted as often as its authored loops repeat it. A Type with leaves at several levels keeps those leaf bytes separate. A UMAT leaf has no residency of its own: when it appears in Call.args, charge its own bytes at the target's established rmem materialization level; when it appears only in an Op attribute, charge nothing. A Function Call takes the callee's grouped total. |
No |
TrafficMetadata.per_unit |
The same one occurrence, asked of the same relations in the analysed level's window, charged at the levels the operand's projected Type names. On a Function, summed over occurrences with the same repetition. A Function Call takes the equivalently projected callee total. | No; projection reads resolved Mesh and effective Module topology extents. |
TrafficMetadata.operands |
One occurrence's per-boundary movement in order (*call.args, call), the same relation-derived amounts whole groups. Empty on a Function and on a Function Call, neither of which has a split. |
No |
The target-aware loop projection is report data rather than another metadata
record. LoopFootprintMetadata remains target-independent:
"cache-pressure": [{"cache_level": <level>, "backing_level": <level>,
"device_bytes": <int>, "capacity_bytes": <int|null>,
"status": "fits"|"exceeds"|"lower-bound"|"unknown"}, ...]
The projection MUST use device_bytes, sum rows at the cache's ultimate explicit
backing level, and compare only levels whose capacity scopes agree. A missing
backing level or a scope mismatch MUST emit no row and MUST NOT fail analysis.
lower-bound means an incomplete footprint has not yet exceeded capacity;
exceeds remains conclusive when the lower bound alone exceeds it. A cache with
no usable capacity emits unknown. A buffer with device_bytes < bytes MUST be
removed before projection and its LoopFootprintMetadata MUST be marked
incomplete.
The family reads this target projection:
class MemoryRelationKind(Enum):
"""How two memory levels are related."""
CACHES = "caches"
SHARES_CAPACITY_WITH = "shares_capacity_with"
class ExplicitMemoryLevelFacts:
"""A level a program places values in by name.
Attributes:
name: attribute; The storage level name.
capacity_bytes: attribute; Stated capacity, or None when unknown.
scope: attribute; The topology level the capacity is stated per.
owner: attribute; The topology whose units own separate values, or target.
"""
name: str
capacity_bytes: int | None
scope: str
owner: str
class ImplicitMemoryLevelFacts:
"""A level traffic passes through without being placed there.
Attributes:
name: attribute; The cache level name.
capacity_bytes: attribute; Stated capacity, or None when unknown.
scope: attribute; The topology level the capacity is stated per.
"""
name: str
capacity_bytes: int | None
scope: str
class MemoryLevelRelation:
"""One edge between two memory levels.
Attributes:
kind: attribute; Which relationship this edge states.
near: attribute; The level closer to the compute units.
far: attribute; The level on the other side of the edge.
shared_capacity_bytes: attribute; Size of the divided block, on a sharing edge.
"""
kind: MemoryRelationKind
near: str
far: str
shared_capacity_bytes: int | None = None
class MemoryHierarchyFacts:
"""Every memory level of one target, as a flat graph.
Attributes:
explicit_levels: attribute; The levels a program names.
implicit_levels: attribute; The levels traffic only passes through.
relations: attribute; The edges between them.
"""
explicit_levels: tuple[ExplicitMemoryLevelFacts, ...]
implicit_levels: tuple[ImplicitMemoryLevelFacts, ...]
relations: tuple[MemoryLevelRelation, ...]
Requesting memory adds the Function's own movement, one footprint line, and one line per advisory:
traffic traffic=<level>:r<int>/w<int>@r<int>/w<int>[,...]
peak-footprint=<level>:<int>[,<level>:<int>...]
advisory="<text>"
An empty footprint states the family name alone; each advisory is its own line
and is quoted and escaped
(inspection §2.8). The record's own
comment form projects the footprint it holds, and lifetimes is read from JSON:
Every measured Call also receives a traffic annotation, whose operands split
is emitted only when asked for (cli Analyze) and is absent
from a Function, which has no split:
Its JSON projection is under the reported value's traffic key, with whole,
per_unit and one operands entry per position carrying read and write.
The analyze equation printer emits no memory annotation because that record is
attached only to the Function. Its full JSON projection is under
function_records.memory:
{"footprint": [{"level": <level>, "peak_bytes": <int>,
"persistent_bytes": <int>, "capacity_bytes": <int|null>}, ...],
"traffic": {<level>: {"read": <int>, "write": <int>}},
"lifetimes": [{"binding": <name>, "level": <level>, "bytes": <int>,
"defined_at": <int>, "last_used_at": <int>,
"persistent": <bool>}, ...],
"advisories": [<text>, ...]}
- constraints:
MemoryMetadataMUST be attached per reachableFunction; a peak spans its live ranges and belongs to no single expression.ReshapeandTransposedescribe bytes their operand already holds and MUST NOT receive independent lifetimes. Every other result, a window and a field of a tuple and one that overwrites a destination included, MUST allocate its own: landing in an operand's buffer is a fact about a plan, and no plan has been made here. Analysis uses operation semantics for this distinction rather than inferring aliasing from layouts.- A caller-owned parameter MUST NOT be reused. Donation is a contract with the caller, not a conclusion this family may draw.
- Which boundaries move is the Op's evaluator's answer and MUST NOT be read
off the lifetimes above: a boundary it reports no direction on moves
nothing, which is what a
Reshapeand aSlicesay of their tensor source and their result, while aTransposeshares its operand's bytes and still reads and writes them because its evaluator materialises the permutation. The numbers that place a window MUST be read like any other operand: one element per number, reached through the boundary's own relation onto the flat leaves the operand holds, and charged at each reached leaf's own width. An operation that writes at an address it is given reads that address the same way. - The memory levels MUST be two flat tuples with a separate relation edge list.
- A GPU projection MUST cover the explicit levels a program can name and the caches traffic passes through, and MUST state that L1 caches L2, that L2 caches global memory, and that L1 divides one physical block with shared memory. A target with no sharing MUST express that with no sharing edge.
- An implicit level MUST NOT receive a fixed capacity where its usable capacity depends on the program; that capacity MUST be derived from the sharing edge and the sharing level's measured peak.
- Every explicit level MUST carry an
ownersupplied by the Target. It MUST be a declared Target topology ortarget. An implicit cache MUST NOT carry an owner. - Analysis MUST NOT infer memory ownership from a storage level's name or capacity scope.
- One value exceeding an explicit level's capacity MUST raise
AnalysisError. An aggregate explicit peak or an authored-loop access footprint exceeding an implicit cache capacity MUST instead produce an advisory and MUST NOT fail the call.
1.2.3 roofline¶
roofline converts recorded work into a lower time bound at the target's
published compute and memory rates.
class RooflineMetadata(IRMetadata):
"""A lower bound on time, and which side of the machine sets it.
Attributes:
compute_ns: attribute; Time the flops imply at the target's rates.
memory_ns: attribute; Time the traffic imply at the target's bandwidth.
ideal_ns: attribute; The ideal bound the two imply.
bound_by: attribute; Which resource set the bound.
"""
compute_ns: int = 0
memory_ns: int = 0
ideal_ns: int = 0
bound_by: str = "none"
| Field | How it is computed | Reads the target |
|---|---|---|
compute_ns |
For each recorded dtype with a published rate, round flops * 1e9 / rate up to ns and sum the dtype times. A Function uses its summed flops, not a sum of per-Call times. |
ThroughputFacts.peak_flops_per_second |
memory_ns |
Add reads and writes at bandwidth_level, multiply by 1e9 / memory_bandwidth_bytes_per_second, and round up to ns; zero when no bandwidth is published or no bytes move. A Function uses its summed traffic, not a sum of per-Call times. |
ThroughputFacts.bandwidth_level and memory_bandwidth_bytes_per_second |
ideal_ns |
Maximum of compute_ns and memory_ns; one ns when the occurrence records nonzero flops or nonzero bandwidth_level traffic and neither published rate yields a bound, otherwise zero. Traffic at any other level is stated and does not earn a bound: no rate was published for it, so none is owed. |
Through the two times |
bound_by |
none for no bound, which includes an occurrence whose only movement is at a level with no published bandwidth, balanced for equal nonzero times, memory when memory is greater, compute when compute is greater, and unrated for the one-ns bound owed by work this prices whose rate is missing. |
Through the two times |
The family reads this target projection:
class ThroughputFacts:
"""Carry the whole-device rates a bound divides the whole program's work by.
Attributes:
peak_flops_per_second: attribute; Published compute rates by dtype.
memory_bandwidth_bytes_per_second: attribute; Published memory bandwidth.
bandwidth_level: attribute; Memory level whose traffic the bandwidth measures.
"""
peak_flops_per_second: tuple[tuple[DType, int], ...]
memory_bandwidth_bytes_per_second: int | None
bandwidth_level: str
def peak_for(self, dtype: DType) -> int | None: ...
What one unit gets through is a separate projection, read by performance
rather than by roofline: work that is not floating point at all has its own
published instruction throughput and no dtype to be filed under.
class PerformanceServiceFacts:
"""Carry everything one unit gets through, by the kind of work it is asked for.
Attributes:
unit_flops: attribute; One unit's floating-point rate, by dtype.
unit_ops: attribute; One unit's rate for each named service kind.
unit_bandwidth: attribute; One unit's rate for each memory level it moves at.
unit: attribute; Topology level these throughputs describe.
"""
unit_flops: tuple[tuple[DType, int], ...]
unit_ops: tuple[tuple[str, int], ...]
unit_bandwidth: tuple[tuple[str, int], ...]
unit: str
def flops(self, dtype: DType) -> int | None: ...
def ops(self, kind: str) -> int | None: ...
def bandwidth(self, level: str) -> int | None: ...
The service kinds a target states are integer, predicate, select and
special. The names are this project's, not any vendor's, so each MUST say in
its provenance which published row it was derived from. A service is work the
machine does, not movement it makes: bytes are priced by a bandwidth, never by
standing an instruction rate in for one.
Requesting roofline adds this verdict and the two quantities the bound divides
-- the summed flops and the bandwidth-level bytes -- as totals. Nothing else
its dependencies wrote is promoted; asking for memory is what states those
(§1.2.2).
Every measured Call receives this annotation. compute_ns and memory_ns are
the two numbers the verdict was read off, so they are in JSON rather than on the
line:
Reported Call and Function records use the same projection under their
roofline keys:
When roofline is requested without its dependencies being requested, totals
carries only exact flops and traffic sums, and function_records.memory
carries only level and peak_bytes per footprint row. Persistent bytes,
capacities, advisories, lifetimes, operand splits, and dependency annotations do
not enter that view. Independently requesting a dependency selects its full form
as defined in that family's section.
- constraints:
ThroughputFacts.peak_forMUST returnNonefor a dtype with no published rate; analysis MUST NOT substitute an assumed rate.PerformanceServiceFacts.flops,opsandbandwidthMUST returnNonefor an unstated dtype, kind or level. Performance MUST reject non-zero work of that dtype, kind or level and MUST NOT substitute the whole-device rate or another kind's rate.bandwidth_levelMUST select the traffic level divided by the published bandwidth rather than summing traffic across levels.- Performance local duration MUST divide
ComputeCostMetadata.flops_per_unitbyunit_flops,service_per_unitbyunit_ops, and thebandwidth_levelentry ofTrafficMetadata.per_unitbyunit_bandwidth. Compute and movement overlap within one occurrence, so its duration is the greater of the two sides rather than their sum. - Traffic at a level with no stated one-unit bandwidth MUST remain visible in
TrafficMetadataand MUST NOT enter a duration: an instruction throughput standing in for a bandwidth prices a move as though it were arithmetic. - Having moved bytes and having work this can time are different questions.
What decides the second is the quantities a rate exists for: nonzero
flops_per_unit, nonzeroservice_per_unit, or nonzeroTrafficMetadata.per_unitatbandwidth_level. An occurrence with none of them MUST take zero time, MUST NOT be required to carry an execution placement, and MUST still record its movement at any other level: it is untimed, not absent. Work of a dtype or kind the target states no one-unit throughput for MUST refuse rather than price at zero, because that would leave a hole inside a number the reader takes as whole. - A predicate MUST NOT be recorded as floating-point work. A comparison
records
predicateservice and a selection recordsselect; neither has a FLOP count, and neither MAY be priced at zero for want of one. flopsMUST stay the measure of arithmetic that really is a multiply or an add. An operation the machine answers on a separate unit at a separate published rate belongs inservice, and MUST NOT also appear inflops: one operation is one quantity, and a roofline that counts a special-function result as one FLOP states a bound the unit cannot meet.- Rate-to-duration divisions MUST use exact integer ceiling division. They MUST NOT pass through floating-point arithmetic.
- Roofline records MUST be attached to every reachable
CallandFunction. - Roofline MUST read
ComputeCostMetadatarather than evaluate the program a second time. - A recorded compute dtype that is not a
DTypename MUST raiseAnalysisError. ThroughputFacts.peak_forMUST returnNonefor an unpublished dtype rate, and analysis MUST NOT substitute an assumed rate.- Roofline reads only
ComputeCostMetadata, so a roofline-only rendering reports only what roofline and that dependency wrote. No other family's conclusion is promoted into it.
1.2.4 performance¶
performance places compute-cost-priced occurrences on a CTA-local nominal
timeline, holds the buffers they keep live to the levels this model addresses,
and scales the root timeline by a fixed physical parallel capacity. The records
it owns are named for the prediction they carry rather than for the selector, so
that the interval stays one nested value with one meaning wherever it appears.
class TimelineMetadata:
"""One interval on the nominal timeline.
Attributes:
start_ns: attribute; Modeled start, in ns.
end_ns: attribute; Modeled end, in ns.
trips: attribute; Number of executions represented by this interval.
stride_ns: attribute; Start-to-start distance between repeated executions.
"""
start_ns: int = 0
end_ns: int = 0
trips: int = 1
stride_ns: int = 0
class PerformanceMetadata(IRMetadata):
"""One occurrence's interval within one local wave of its Function.
Attributes:
timeline: attribute; That occurrence's CTA-local interval.
"""
timeline: TimelineMetadata
class PerformanceSummaryMetadata(IRMetadata):
"""One Function's predicted time, and what reaching it took.
Attributes:
timeline: attribute; Whole-Function envelope from zero, in ns.
waves: attribute; Physical waves required by the root topology.
"""
timeline: TimelineMetadata
waves: int
TimelineMetadata is a value, not a record: it MUST NOT be attached to a Call or
a Function on its own, and what it spans is stated by the record carrying it.
Occurrence fields are:
| Field | How it is computed | Reads the target |
|---|---|---|
start_ns |
Start of one occurrence on the authored-order local timeline, after its producers end and after the last occurrence sharing any of its participants. | No |
end_ns |
End of that occurrence's first execution. | No |
trips |
One outside a loop; within a loop, the enclosing loop trip count represented by the interval. | No |
stride_ns |
Zero outside a loop; within a loop, the makespan of one body execution. | No |
Function summary fields are:
| Field | How it is computed | Reads the target |
|---|---|---|
timeline |
[0, local makespan * waves), where the local makespan is the end of the CTA-local timeline, or zero with no work. Its duration is the prediction. |
Through waves |
waves |
ceil(N / P), where N is the static extent of the root topology selected by ParallelCapacityFacts.topology and P is parallel_units. |
ParallelCapacityFacts |
Occurrence intervals remain CTA-local. They are not copied once per wave, and
neither the root topology extent nor parallel_units changes them. The capacity
P is compiler policy for concurrent instances; it is distinct from the
per-unit rates in ThroughputFacts even when both projections derive from the
same physical unit count today.
The family reads this target projection:
class ParallelCapacityFacts:
"""Carry the parallel capacity assumed by performance analysis.
Attributes:
topology: attribute; Topology level being measured over.
parallel_units: attribute; Instances admitted concurrently.
"""
topology: str
parallel_units: int
Requesting performance adds this Function verdict to the summary:
root is the report's own identity, composed by inspection from the module and
function it already states; it is not a field of PerformanceSummaryMetadata and
does not appear in that record's JSON projection. predicted-ns is the duration of
the summary's own envelope, not a second measurement. waves is stated even when
it is one, because how many passes over the machine a plan takes is a conclusion
and one wave is an answer.
Every Call with a modeled duration receives this annotation, one interval whether or not it repeats: a single trip states its own bounds, and a repeated occurrence states them offset by the trip index, with the trip count as a suffix. The trip count is not a second key, because a reader deriving the later intervals reads it off the interval it is a coefficient in:
Reported Call and Function records use distinct projections under their
performance keys:
Call: {"timeline": {"start_ns": <int>, "end_ns": <int>, "trips": <int>,
"stride_ns": <int>}}
Function: {"timeline": {"start_ns": 0, "end_ns": <int>, "trips": 1,
"stride_ns": 0},
"waves": <int>}
The summary envelope's duration is a deterministic comparison estimate, not a runtime prediction. It deliberately excludes launch overhead, occupancy, utilization, traffic volume, and other execution effects that this family does not model.
- constraints:
- A primitive Call is eligible for performance when it is reachable in the
authored HIR. Its execution region is represented structurally by
MeshScope; the result layout remains an independent property. A result carrying noShardLayoutMUST NOT unplace the occurrence that produced it. An occurrence with no nonzeroflops_per_unit, no nonzeroservice_per_unitand no nonzeroTrafficMetadata.per_unitatbandwidth_levelis structural to this model: it needs no execution placement and MUST receive no record, because an empty interval reads as a measurement rather than as the absence of one. Movement at another level does not change that and MUST NOT be dropped fromTrafficMetadatabecause of it -- structural here means nothing to time, not nothing done. It still carries its producers' precedence to its consumers. Inputs MUST NOT supply placement for an unplaced occurrence. - The global total for an occurrence is its per-unit quantity multiplied by the number of positions in the enclosing execution scope. This multiplier counts copied or replicated work: an unsharded operation inside a scope is performed independently by every position and therefore contributes once per position to the total. The per-unit quantity already includes work projected through finer levels.
- A
MeshScopeevaluates its boundary arguments outside the region and executes its body once per enclosing position. Argument work is charged at its defining site; body work is charged per enclosing position. - The participant set MUST be the exact image of that Mesh's layout under
shard §5, not an extent inferred from a topology or an
operand. A
Broadcastshard attribute still names placement: attributes describe distribution while the Mesh describes which positions participate. - Every primitive occurrence has one fixed duration and occupies its exact participant set. An SSA consumer MUST start no earlier than its producers end. Two positive-duration occurrences whose participant sets intersect MUST NOT overlap; disjoint sets MAY overlap, while a partial intersection serializes each whole occurrence rather than splitting it by participant.
- A
GridRegionExprMUST be represented as one structured performance node. Its body is solved once, from the time the loop itself begins rather than from zero, so a body occurrence's reported[start_ns, end_ns)is the interval it actually runs in and not one a reader has to offset.stride_nsis that body's local makespan and the t-th execution of a body occurrence with first interval[start_ns, end_ns)is[start_ns + t*stride_ns, end_ns + t*stride_ns), for0 <= t < trips. The loop spanstrips * stride_ns; a consumer of its yield MUST wait for that full span. Loop-invariant values remain single occurrences outside it. - What an occurrence waits for MUST be read off the program's own structure: the values it names, the loop it sits in, and the participants it runs on. An ordering MUST NOT be inferred from an allocation -- which values share bytes is a plan's decision and no plan has been made -- and no occurrence is held back for a write nobody proved happens in place.
- Occurrences MUST be laid out in inline occurrence order. Reordering
independent work is a later decision, not an analysis's: what overlaps
is what the program's own placement made independent, and the reported time
is the time of the program as written. On a
Function, the summary'stimelineMUST start at zero and span the whole local plan, scaled bywaves; there MUST be no second field restating the local makespan or the scaled estimate, and none restating how the layout was reached -- it is exact for the model it states. parallel_unitsis compiler policy over hardware facts. It MUST NOT enter one-unit rates or the CTA-local layout, and is not a program rewrite.- The buffers a plan keeps live MUST have been placed by
memorybefore a time is reported for it, which aMemoryMetadatacarrying anallocationis the evidence of; one without it MUST fail withAnalysisError. A placement that failed never reaches here, becausememoryrefuses it. Capacity therefore changes whether there is an answer, never which answer: two capacities that both admit a placement MUST produce the same intervals. - Performance is a modeled plan and MUST NOT be read as a guarantee about lowering, physical occupancy, or runtime performance.
2. Composed analysis¶
tilefoundry.analysis.check_program is the shared, reusable gate before an
analysis runs.
def check_program(
module: "Module",
function: "Function",
*,
level: str | None = None,
budget: int = _INLINE_NODES,
analyzers: tuple["Analyzer", ...] = (),
) -> "Function": ...
class AnalysisCheckContext:
"""What every input check reads: the program, the machine, and the level.
Costing here is a question, not a record: nothing a context computes is
attached.
"""
module: "Module"
function: "Function"
target: "Target"
level: str | None
whole: CostContext
local: CostContext
- constraints:
- The operation MUST infer types over the full reachable Function graph and validate its caller/callee execution context, and MUST NOT run an analysis or attach derived Metadata to the authored IR.
- The reachable Function and Mesh geometry and every effective Module
topology extent MUST be concrete before this operation runs. A public
Analyze call with
dimsMUST resolve all three through one binding pass before calling this gate; a residual dimension expression MUST fail before any consuming algorithm runs. - Every effective Module topology MUST name a level the resolved Target supports. A resolved static extent MUST be positive and within that level's finite hardware limit. A rejection MUST name the level, its extent, and the reason.
- A non-
NonelevelMUST name exactly one effective Module topology. - Analyze MUST call this operation before any consuming algorithm, and
MUST pass the whole resolved dependency closure as
analyzers, so every analysis about to run states its input contract here. - The
analyzerscheckers MUST be bound to the derived Function and run in closure order: everycheck_target, then everycheck_callover one traversal of the derived non-Function calls, then everyfinish. One program MUST be walked once for this however many analyses asked, and the first refusal MUST stop the gate before any analysis writes. - The returned Function MUST inline every reachable HIR Function call at its
call site while retaining each
GridRegionExpras one loop. Its induction variable, carried values, and yields MUST NOT be replaced with iterations. The authored Module and Function MUST remain unchanged. - The returned Function parameters MUST be the authored entry parameters
followed by the
ConstTensordeclarations needed by reachable Module readings. A promoted declaration MUST be named by the clean dot-joined Module path and weight name used by runtime checkpoint keys. Declarations MUST follow the Module tree's owner-before-children order, and within one Module are unioned by name in Function/parameter order. Separate attachment paths MUST remain separate resources. Unequal types for one(module path, weight name)MUST fail. No constant value enters the IR. - Every primitive Call in the returned view MUST have a deterministic unique binding.
budgetMUST be a non-negative integer limiting the number of unique body expression nodes after inlining. An oversized view MUST fail with both its size and the limit and MUST NOT return a partial Function.- Authored-analysis readiness is not a program-level rejection. Analyze MUST
NOT reject an authored
where(...)constraint: it is an input to a later decision, and a program carrying one is measured as written. A value whose placement is deferred contributes its whole-program figures to the per-unit total, because a deferred layout states no distribution to project through; values with a resolved layout still project.
tilefoundry.analysis.api.analyze is the dependency-composed measurement
operation. One call selects one or more root analyses by name; the operation
resolves their union dependency closure, runs each member once, and reports what
ran.
Its subject is one Module and one HIR Function that Module owns. Reachable
HIR callees are part of that selected invocation and do not become separate
launches because of Module ownership; the invocation rule is owned by
hir §1.1. Analyze does not select, interpret, trace, or
dummy-run a plain Python orchestration method.
- constraints:
- Analyze MUST validate every caller/callee edge the selected query reaches
against the one-execution-context requirement
(hir §1.1). Reaching is what is validated, so an
attached child no call reaches has no edge here. Of the two resolved values
only the topology hierarchy is compared: the
Targetneeds no second check, because only a root declares one (core-ir §1). - The Module owning a reached
FunctionMUST be answered within the supplied tree, by identity and recorded origin rather than by name. No owner, or more than one, is refused rather than assigned to the root.
class AnalysisResult:
"""Record what one composed Analyze call computed.
Attributes:
module: attribute; Source Module.
function: attribute; Function that received records.
analyses: attribute; Requested root analyses in first-occurrence order.
level: attribute; Topology level whose unit the per-unit quantities describe, or None.
executed: attribute; Analyses executed in dependency order.
metadata_types: attribute; Metadata classes actually written.
"""
module: "Module"
function: "Function"
analyses: tuple[str, ...]
level: str | None
executed: tuple[str, ...]
metadata_types: tuple[type[IRMetadata], ...]
def analyze(
module: "Module",
function: "Function",
*,
analysis: str | Iterable[str],
level: str | None = None,
options: object | None = None,
dims: "Mapping[str, int] | None" = None,
) -> AnalysisResult: ...
- constraints:
- One call MUST select one or more root analyses. It MUST preserve their first-occurrence order, resolve their union dependency closure, and execute every member once.
levelMUST name one effective Module topology. When omitted, it MUST default to the coarsest effective topology; when the Module declares none, it MUST remainNoneand no per-unit projection divides.AnalysisResult.levelMUST record the resolved answer.- The Function MUST be one the Module owns: one it declares, or a specialization variant of one it declares (core-ir §1). A Function derived by specialising one of these MUST be refused, so that ownership is settled before anything is rebuilt.
dimsstates one extent per dimension reached through the Function graph, its Mesh geometry, or the effective Module topology expressions. An analysis counts elements and holds them against a machine, and has no answer for a range in any of those positions, so the program MUST be analysed at a chosen size rather than as authored.dims=NoneMUST behave as a call that states no size: the Function is analysed as authored before the shared program check builds the inlined view, andAnalysisResult.functionMUST be that record-bearing view.- When
dimsis stated it MUST be non-empty; every key MUST name a dimension reached through the Function graph, its Mesh geometry, or the effective Module topology expressions; every value MUST be an integer inside that dimension's declared bounds; every dimension the Function selects a variant on MUST be given a value; and no dimension MAY remain a range after substitution. Each of these MUST fail with an Analysis domain error. A stateddimsMUST NOT be silently ignored, including when the Function declares no range at all. - Variant resolution and substitution MUST happen after the ownership check and before the shared program check or any algorithm runs. Function types, Mesh geometry, and effective topology extents MUST use the same resolved binding. Exactly one variant MUST cover the stated size; none and more than one MUST both fail.
- When
dimsis stated,AnalysisResult.functionMUST be the inlined view of the concrete Function the records were written onto and MUST retain the specialised Function's origin and extents.AnalysisResult.moduleMUST remain the Module the caller supplied. A reader given the symbolic input would find no records on it. - The recorded extents MUST be what identifies which size a derived Function is at. They MUST NOT be inferred from its signature: a dimension occurring only in a loop bound, a body operation's attribute, or a nested callee leaves the signature identical at every extent, so two sizes would be indistinguishable to anything comparing signatures.
- The operation MUST resolve the root's full transitive dependency closure,
order it so every dependency precedes its dependants, and execute each
member exactly once per call.
executedMUST report that order, so a shared dependency appears once. - Dependencies MUST be resolved under the same exact concrete Target as the
root, obtained from
Module.resolve_target(). - A dependency cycle MUST fail and MUST name the path that closes it. A missing root and a missing dependency MUST be distinguishable: one is the caller's selector, the other a broken Target capability.
- Type inference and validation MUST each run once per call, before any analysis. No analysis MAY run once either has rejected the IR, because an analysis reads inferred types and assumes a verified function.
- Family-specific readiness MUST be checked on that inferred inlined view and MUST complete before the first analysis in the dependency closure runs. In particular, a performance request that lacks an execution placement MUST fail before dependency Metadata is written.
- Re-running MUST recompute the closure and refresh the Metadata that closure owns. There MUST be no cross-call cache. Metadata owned by nothing in the closure MUST be left untouched.
metadata_typesMUST list the Metadata types the call actually wrote onto the IR, in execution order and without repeats. An analysis that declares a type but writes no record for this function MUST NOT contribute it, so a renderer is never sent after records that are not there.AnalysisResultMUST be semantic. Human text, JSON, and annotated HIR are renderings of it and of the Metadata on the IR, and MUST NOT be fields of it.
2.1 Shared Scope and Access¶
The normalized HIR is visited once per analyze() call. That visit produces a
Scope tree parallel to Function/GridRegionExpr nesting and Access relations
for the narrow and device views. Scope.domain is the accumulated authored
loop domain; Scope.accesses and Scope.refused are the only family inputs for
loop footprints, movement, and placement. An Access stores only its relation
and allocation expression; storage level and element width are read from the
allocation type. A refused descendant makes its owning scope unknown for that
view. Non-affine runtime indices retain the widest legal access approximation.
Normalization clones each reached Function call site independently. Within one
call site, source expressions shared by identity remain one shared expression
in the clone; sharing never aliases the independently cloned body of another
call site.
2.2 Target-selected Analyzers¶
class AnalyzeContext:
module: Module
target: Target
level: str | None
options: object | None
root: Scope
current: Scope
AnalysisCallable = Callable[
[Function, AnalyzeContext], None
]
class Analyzer:
"""Describe one Target-selected analysis.
Attributes:
selector: attribute; Public analysis selector.
run: attribute; Analysis implementation.
requires: attribute; Dependency selectors.
produces: attribute; Owned metadata classes.
input_checker: attribute; What this analysis needs of a program.
"""
selector: str
run: AnalysisCallable
requires: tuple[str, ...] = ()
produces: tuple[type[IRMetadata], ...] = ()
input_checker: AnalysisInputChecker = NO_INPUT_CHECK
class AnalysisInputChecker(Protocol):
"""What one analysis requires before any analysis writes.
Three questions answerable without reading a record: what the target must
state, what each call must carry, and what the function must hold.
"""
def check_target(self, ctx: AnalysisCheckContext) -> None: ...
def check_call(self, call: Call, ctx: AnalysisCheckContext) -> None: ...
def finish(self, function: Function, ctx: AnalysisCheckContext) -> None: ...
class Target:
def get_analyzer(self, selector: str) -> Analyzer: ...
- constraints:
AnalysisCallableMUST receive the normalized Function graph and oneAnalyzeContextcarrying the exact Module, Target, resolved topology level, caller options, and the shared root/currentScopeview. The level MAY beNoneonly when the Module declares no topology; options MAY beNone.- Analyze MUST obtain every root and dependency from the same exact Target
instance through
get_analyzer. - A Target subclass MUST inherit its base Analyzers through normal Python
inheritance. It MAY override one selector and delegate the rest to
super()or refuse inherited behavior that is invalid for its hardware. - There MUST be no public analysis registration step or exact-concrete-Target algorithm table. A custom provider registers only its Target class.
- A declaration MUST be rejected when it requires itself, repeats a
dependency, produces the same Metadata type twice, or names a
producesentry that is not anIRMetadatasubclass. input_checkerMUST default to one that requires nothing, so an analysis with no input contract is declared by leaving the field out and keeps working unchanged. A checker MUST NOT attach Metadata: it states what a program must already be, and every checker in a closure MUST have answered before any analysis in it writes.- An analysis MAY change only the Metadata types its Analyzer declares. Ownership MUST be enforced against what reached the IR rather than against what the analysis reports, and MUST cover addition, replacement, and removal alike: deleting another analysis's record changes the IR as much as overwriting it. An equal-valued overwrite of another analysis's record MUST also count as a violation.