Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5cedb0d
feat(hir): add value range metadata
zhen8838 Sep 21, 2026
307c597
fix(hir): preserve cached value ranges
zhen8838 Sep 21, 2026
0dae330
fix(hir): narrow symbolic dimension detection
zhen8838 Sep 21, 2026
1989df1
feat(analysis): support bounded loop endpoints
zhen8838 Sep 21, 2026
2433684
fix(analysis): enumerate bounded parameter domains
zhen8838 Sep 21, 2026
67273b6
fix(analysis): reject unknown trip counts
zhen8838 Sep 21, 2026
4416a68
fix(analysis): bind access parameters by name
zhen8838 Sep 21, 2026
9325b55
feat(analysis): distinguish bounded access parameters
zhen8838 Sep 21, 2026
cc2889a
feat(parser): support unit-dependent tile bounds
zhen8838 Sep 21, 2026
d31835d
feat(analysis): validate unit-dependent loop starts
zhen8838 Sep 21, 2026
448d74c
fix(analysis): preserve converter and realistic fixture coverage
zhen8838 Sep 21, 2026
66cd89c
refactor(parser): simplify reviewed loop-bound paths
zhen8838 Sep 21, 2026
9bc10d1
docs(analysis): structure parameter constraints
zhen8838 Sep 21, 2026
94681a9
test(analysis): remove superseded persistent coverage
zhen8838 Sep 21, 2026
67ebcf4
refactor(types): colocate ShapeDim with tensor types
zhen8838 Sep 21, 2026
daa3396
refactor(analysis): separate iteration scope responsibilities
zhen8838 Sep 21, 2026
279643b
refactor(analysis): centralize enclosing loop traversal
zhen8838 Sep 21, 2026
890e9c1
refactor(analysis): unify cardinality counting
zhen8838 Sep 21, 2026
4931970
test(analysis): define empty parameter cardinality
zhen8838 Sep 21, 2026
2a657b5
refactor(types): clarify dim isl boundaries
zhen8838 Sep 21, 2026
bedef60
refactor(visitor-registry): isolate type inference visitor
zhen8838 Sep 21, 2026
de334e5
test(analysis): cover persistent schedule invariants
zhen8838 Sep 21, 2026
f18b3f4
refactor(ir): move isl interop out of types
zhen8838 Sep 22, 2026
1942765
refactor(analysis): clarify loop term ownership
zhen8838 Sep 22, 2026
713390c
refactor(analysis): publish shared analysis helpers
zhen8838 Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions docs/spec/analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@ is a conclusion of this analysis. `rmem` is not address-solved and reports only
the largest single projected logical value.

- constraints:
- An access relation that keeps a parameter with a stated finite range is
exact and MAY prove overlap. A widened relation, and one with an unbounded
parameter, MUST NOT.
- Placement MUST be settled for the addressable levels `gmem` and `smem` only,
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
Expand Down Expand Up @@ -1203,12 +1206,13 @@ def analyze(
renderings of it and of the Metadata on the IR, and MUST NOT be fields of
it.

### 2.1 Shared Scope and Access
### 2.1 Shared IterationScope and Access

The normalized HIR is visited once per `analyze()` call. That visit produces a
`Scope` tree parallel to Function/LoopRegion 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
`IterationScope` tree parallel to Function/LoopRegion nesting and `Access`
relations for the narrow and device views. `IterationScope.domain` is the
accumulated authored loop domain; `IterationScope.accesses` and
`IterationScope.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
Expand All @@ -1218,6 +1222,27 @@ 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.

`analysis.loop_terms` resolves HIR values to `LoopTerm` without depending on
isl. `analysis.access` turns those terms into isl constraints and owns access
widening; the two modules do not define a second affine graph representation.

- constraints:
- A loop `start` or `extent` MAY be unit-dependent. Every runtime leaf in one
MUST carry a half-open value range, and `IterationScope.domain` MUST keep the
whole affine expression with each such leaf as one identity-deduplicated
isl parameter constrained by that range. A leaf without a range MUST be
refused.
- A loop `step` MUST be a literal; a parametric stride has no isl
representation.
- `cardinality` MUST enumerate every feasible integer point of a parameter box
of at most `PARAM_POINT_LIMIT` points and return the maximum, and MUST
report unknown for a larger box. It MUST count directly when every retained
parameter is already fixed to one integer point. An empty parameter context
MUST count as zero; a non-empty context with an unbounded parameter MUST
report unknown.
- `IterationScope.trips()` MUST fix child and parent domains to the same parameter
point before dividing, and take the maximum of those ratios.

### 2.2 Target-selected Analyzers

```python
Expand All @@ -1226,8 +1251,8 @@ class AnalyzeContext:
target: Target
topology_level: str | None
options: object | None
root: Scope
current: Scope
root: IterationScope
current: IterationScope


AnalysisCallable = Callable[
Expand Down Expand Up @@ -1271,7 +1296,7 @@ class Target:
- constraints:
- `AnalysisCallable` MUST receive the normalized Function graph and one
`AnalyzeContext` carrying the exact Module, Target, resolved topology level,
caller options, and the shared root/current `Scope` view. The
caller options, and the shared root/current `IterationScope` view. The
`topology_level` MAY be `None` only when the Module declares no topology;
options MAY be `None`.
- Analyze MUST obtain every root and dependency from the same exact Target
Expand Down
13 changes: 8 additions & 5 deletions docs/spec/code-organization.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ truth for the directory's structure and invariants.
| `ir/types/shard/` | [shard](./shard.md) | Shard / layout sublayer: `Topology` / `Mesh` / `Layout` / `ComposedLayout` / `ShardLayout` / `ShardAttr` (`Split` / `Broadcast` / `Dynamic` / `Partial`). The physical nesting reflects the spec's "sublayer" relationship. |
| `ir/constraints/` | [parser](./parser.md) | Authored `where(layout=..., mesh=..., storage=...)` constraint records: the shared base plus layout, mesh, and storage constraints, attached by the parser and read back by the Python printer. |
| `ir/visitor.py` | [visitor-mutator](./visitor-mutator.md) | `ExprFunctor` / `ExprVisitor` / `ExprWalker` / `ExprCollector` / `ExprCloner` / `BindingSubstitutionCloner` / `StmtVisitor` / `StmtMutator` / `StmtExprMutator`, plus `collect_exprs`, value-operand/function-value queries, and the canonical `PrimFunction` walk and rewrite entries. |
| `ir/isl_interop.py` | [types](./types.md) | Interoperation between dimension and shape IR values and isl: expression rendering and decoding, normalization, value ranges, and shape-domain construction. Pure isl operations remain in `utils/isl_utils.py`. |
| `ir/hir/` | [hir](./hir.md) | HIR Op layer; one subdirectory per category (`math/` / `tensor/` / `nn/` / `shape/` / `sharding/`). One real Op per `.py` ([§2](#2-file-naming-and-content-rules) rule 1); surface-alias schemas have no per-name file and live in each category's `aliases.py` ([§2](#2-file-naming-and-content-rules) rule 5). |
| `ir/tir/` | [tir](./tir.md) | TIR layer: `stmt.py` re-exports the `Stmt` base from `ir/core/stmt.py`; `stmts.py` hosts the general TIR `Stmt` subclasses (`LetStmt` / `Evaluate` / `Sequential` / `MeshScope` / …); `prim_function.py`; effect Ops and TIR-owned Expr Ops by category (`memory/` / `nn/` / …); `launch.py` owns `Launch` and its authored launch-attribute descriptors; `arith.py` / `reduce.py` for tag-dispatched `Binary` / `Unary` / `Reduce`; `intrinsic.py` for the `@intrinsic` decorator. Target-specific nodes nest under `ir/tir/<target>/<category>/` (e.g. `ir/tir/cuda/nn/mma.py`) per [§2](#2-file-naming-and-content-rules) Rule 1c. |
| `parser/` | [parser](./parser.md) | DSL → IR parsing: `base.py` (shared visitor base + dispatch), `hir_parser.py` (`@func` body), `tir_parser.py` (`@prim_func` body), layout sugar / range-slice / dispatch modules. **Not under `ir/`** — the parser is a producer of IR, not an IR sublayer. |
Expand All @@ -34,18 +35,20 @@ truth for the directory's structure and invariants.
| `analysis/api.py` | [analysis](./analysis.md) | The public composed Analyze operation: shared authored-program check and normalization, one per-call `AnalyzeContext`, dependency closure, ordering, single execution per member, Metadata-ownership enforcement, and semantic result assembly. |
| `analysis/registry.py` | [analysis](./analysis.md) | The built-in Analyzer declarations. It re-exports the immutable `Analyzer` descriptor from `target/services.py` and holds no Target dispatch table. |
| `analysis/errors.py` | [analysis](./analysis.md) | `AnalysisError`, the one diagnostic the whole analysis layer raises, so catching an analysis failure catches every analysis failure rather than the subset the caller happened to import. |
| `analysis/visitor.py` | [analysis](./analysis.md) | The per-call `AnalyzeContext`, carrying the shared root/current lexical `Scope` while a family traverses its work. |
| `analysis/scope.py` | [analysis](./analysis.md) | The shared `Scope` tree and `Access` relations built once from normalized HIR; families query these views instead of constructing parallel structure. |
| `analysis/affine.py` | [analysis](./analysis.md) | The shared loop-affine term parser used by scope binding and authored-loop footprint binding, including constant loop strides and bounded invariant offsets. It does not introduce a second affine graph representation. |
| `analysis/footprint.py` | [analysis](./analysis.md) | Target-independent authored-loop access images, buffer-view folding, and deduplicated versus repeated byte readings. Requires no separate time map. |
| `analysis/visitor.py` | [analysis](./analysis.md) | The per-call `AnalyzeContext`, carrying the shared root/current `IterationScope` while a family traverses its work. |
| `analysis/iteration_scope.py` | [analysis](./analysis.md) | The shared `IterationScope` tree built once from normalized HIR; families query it instead of constructing parallel structure. |
| `analysis/access.py` | [analysis](./analysis.md) | Access relations resolved against the authored iteration scopes. |
Comment thread
zhen8838 marked this conversation as resolved.
| `analysis/loop_domain.py` | [analysis](./analysis.md) | isl iteration domains built from authored loop bounds. |
| `analysis/loop_terms.py` | [analysis](./analysis.md) | IR-only resolution of constants, authored loop axes, strides, and bounded invariant offsets into `LoopTerm`; it has no isl dependency. |
| `analysis/footprint.py` | [analysis](./analysis.md) | Reserved for future target-independent authored-loop footprint policy; access-relation construction stays in `analysis/access.py`. |
| `analysis/report.py` | [analysis](./analysis.md) | Structured analysis report data, including record-family registration, field serialization, and target-aware report-only projections. It depends only on analysis/core modules; inspection consumes it to produce text and source annotations. |
| `analysis/check.py` | [analysis](./analysis.md) | The shared authored-program gate for analysis: authored-type re-derivation, authored validation, call-context validation, and checker-specific input checks. Established once per public call rather than per family. |
| `analysis/facts.py` | [analysis](./analysis.md) | The narrow Facts aggregates the analysis families declare — the memory hierarchy graph, the throughput rates, and the parallel capacity. It is the record of how much hardware each measurement rests on, and names no backend; a Fact shared across consumer families belongs in `target/facts.py`. |
| `analysis/metadata.py` | [analysis](./analysis.md) | The typed records the families leave on the IR, split by what each number depends on rather than by convenience. |
| `analysis/compute_cost.py` | [analysis](./analysis.md) | The `compute-cost` family: logical flops per DType and bytes per storage level, from the authored program alone. |
| `analysis/memory.py` | [analysis](./analysis.md) | The `memory` family: value lifetimes, per-level peaks, and the capacity comparisons against a target's hierarchy — failing on an over-full addressable level and advising on an over-full cache. |
| `analysis/roofline.py` | [analysis](./analysis.md) | The `roofline` family: the recorded work divided by the target's published rates, per Call and aggregated per Function. Adds no count of its own. |
| `analysis/performance.py` | [analysis](./analysis.md) | The `performance` family: occurrences projected from the shared `Scope` tree into flat timeline records and one function envelope, scaled by parallel capacity. It introduces no second scope tree. |
| `analysis/performance.py` | [analysis](./analysis.md) | The `performance` family: occurrences projected from the shared `IterationScope` tree into flat timeline records and one function envelope, scaled by parallel capacity. It introduces no second scope tree. |
| `visitor_registry/` | [visitor-registry](./visitor-registry.md) | Shared registry instances and derived visitors: access-relation construction, contexts, ISL helpers, relation building, shard propagation, type inference, verification, code generation, and cost evaluation. |
| `visitor_registry/op_cost.py` | [analysis](./analysis.md) | Each operation's per-instance flops and bytes, registered into the shared cost-evaluator registry. Owned here rather than by any target package, because the work an operation asks for follows from its own semantics and operand types on every backend. |
| `inspection/analysis_report.py` | [inspection](./inspection.md) | Presentation of analysis-owned report data as text and annotated source. Analysis owns the structured report data and JSON dump; inspection owns how a human reads it. |
Expand Down
23 changes: 15 additions & 8 deletions docs/spec/hir.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ class Function(Expr):
rules are stated below.
- mutable during the compiler's authorised typing, metadata, and specialization updates;
fields that are not updated retain structural equality and hashing semantics.
- value-range metadata is refreshed by a non-type-writing whole-function
inference walk. Parser construction MAY attach an already-proved range to a leaf
whose value is fixed by lexical geometry; `MeshCoord` uses its concrete mesh
axis extent for this purpose.
- a `Function` MUST NOT declare or override execution context. The `Module`
that owns it declares the `Target` and the ordered `Topology` hierarchy its
body runs against ([core-ir §1](./core-ir.md#1-module)).
Expand Down Expand Up @@ -360,14 +364,15 @@ binds a parser-side Python `slice`, while `range` binds a scalar; see
[parser §2.1](./parser.md#21-syntax)). `range` is not unrolled. `induction_var` ranges
over `range(start, extent, step)`: `start` and `extent` are the **half-open**
`[start, extent)` Python-range endpoints (so `extent` is the **stop** value,
not a count). `start` defaults to `0` (`tile(...)` and `range(stop)`); the
`range(start, stop[, step])` surface sets it. Each of `start` / `extent` /
`step` is a `ShapeDim` ([types §4](./types.md#4-dim--symbolic-shape-dimensions)).
not a count). `start` defaults to `0` for `tile(stop, step)` and `range(stop)`;
the `tile(start, stop, step)` and `range(start, stop[, step])` surfaces set it.
Each of `start` / `extent` / `step` is a `ShapeDim`
([types §4](./types.md#4-dim--symbolic-shape-dimensions)).

For a two-argument `tile(extent, step)`, the parser-side window at one
iteration is `[induction_var, induction_var + step)`. The induction value is
already a coordinate in `range(0, extent, step)`, not an ordinal to multiply by
`step`.
For `tile(stop, step)` or `tile(start, stop, step)`, the parser-side window at
one iteration is `[induction_var, induction_var + step)`. The induction value
is already a coordinate in `range(start, stop, step)`, not an ordinal to
multiply by `step`.

- When `start` / `extent` / `step` are static `int`, the trip count is
recoverable from the node alone, without the parser-side window binding
Expand Down Expand Up @@ -1686,4 +1691,6 @@ def is_concrete(fn: Function) -> bool:
re-pointed instead would answer both with whichever was written last.
- `residual_dims` and `dim_vars_reached` MUST inspect the whole function
graph, including signatures, bodies, Op attributes, loop bounds, variants,
and called functions. `is_concrete` additionally checks the return type.
and called functions. `is_concrete` additionally checks the return type and
is false exactly when a reachable required extent still contains a `DimVar`;
runtime values without a `DimVar` are rejected later by their consumer.
2 changes: 1 addition & 1 deletion docs/spec/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ signature ::= (name ':' type-annotation (',' name ':' type-annotatio
return-type ::= type-annotation
if ::= if cond-node block (block)?
while ::= while cond-node block
loop-iterator ::= 'tile' '(' expression ',' expression ')'
loop-iterator ::= 'tile' '(' expression ',' expression (',' expression)? ')'
| 'range' '(' (expression | expression ',' expression | expression ','
expression ',' expression) ')'
loop-carry-statement ::= expression '=' expression
Expand Down
3 changes: 2 additions & 1 deletion docs/spec/passes.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ parser already runs eager typeinfer ([parser](./parser.md)), so a
pass runs, `PassManager` re-runs the relevant analysis on that
pass's **dirty scope**:

- HIR-side: changed `Function`s rerun `typeinfer`.
- HIR-side: changed `Function`s rerun the existing checks and refresh available
`RangeMetadata` over the complete function without rewriting `.type`.
- TIR-side: changed `PrimFunction`s rerun `verify`, which
recursively retriggers `typeinfer` on the embedded Expr fields
and refreshes their `.type`.
Expand Down
23 changes: 23 additions & 0 deletions docs/spec/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,8 +525,31 @@ def ceildiv(a, b) -> Expr:
- `is_dim_expr` MUST accept non-boolean integers, `DimVar`, integer-valued
`Constant`, and recursively valid calls to the seven dimension arithmetic
operations, and MUST reject other values.
- `has_symbolic_dims(value)` MUST report whether a `DimVar` is reachable, not
whether dimension arithmetic contains a runtime `Expr`. A dimension call
recursively checks its operands; non-dimension expressions do not become
symbolic merely by participating in that arithmetic.
- `ceildiv(a, b)` MUST compose the existing add, subtract, and floor-divide
operations; it does not introduce a distinct Op.
- `ir.types.dim` MUST own dimension IR definitions, construction, and
structural predicates without depending on isl. `ir.isl_interop` MUST own
conversion between dimension and shape IR values and isl, affine
normalization, shape-domain construction, and conservative value-range
queries.
- `dim_to_isl_expr` MUST render one dimension expression while registering
its leaf parameters; `isl_to_dim` MUST decode an isl affine expression using
that parameter map. `shape_to_isl_domain` MUST return one shape's iteration
domain and parameter map.
- `index_set` MUST be the non-negative, all-literal shape specialization of
`shape_to_isl_domain`. It MUST return `None` for a negative, boolean, or
non-literal extent rather than constructing a symbolic or empty domain.
- `dim_range(value)` MUST return conservative half-open bounds from
`RangeMetadata` before attempting structural dimension arithmetic. A value
with neither stored nor structurally derivable bounds returns `None`.
Unsupported symbolic divisors remain an error rather than an unknown range.
- A bounded non-dimension `Expr` leaf in dimension arithmetic MUST become one
identity-deduplicated isl parameter carrying its stored bounds. An unbounded
leaf remains an unconstrained parameter for consumers that permit one.

---

Expand Down
Loading
Loading