TileFoundry Spec — Code Organization¶
Implementation guide (not an architecture spec). Defines the Python source tree layout.
1. Directory skeleton¶
This spec describes the stable layers only — whether a single
.pyfile exists is decided by the naming rules in §2 and is not enumerated here (so adding a new Op does not require a spec edit). The current file list is whatevergit ls-files src/tilefoundryreports.
The top-level package src/tilefoundry/ is divided as follows. Each
directory has an owning spec — that spec is the single source of
truth for the directory's structure and invariants.
| Directory | Owning spec | Contents |
|---|---|---|
ir/core/ |
core-ir | Shared node algebra: Module / Expr / Var / Constant / Tuple / Op / Call / Stmt (base class) / OpSchema / ParamDef / call-graph and ownership queries / typed metadata attach-detach and diagnostics / @register_op / @register_alias / op_registry / errors. |
ir/types/ |
types | Type-system root: Type / TensorType / TupleType / UnitType / CallableType / DType / StorageKind / resolve_storage / local projections (local_type_of) / tensor-leaf, byte-by-storage, and topology-extent queries / dim.* (with their typeinfer). |
ir/types/shard/ |
shard | 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 | 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 | 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/hir/ |
hir | HIR Op layer; one subdirectory per category (math/ / tensor/ / nn/ / shape/ / sharding/). One real Op per .py (§2 rule 1); surface-alias schemas have no per-name file and live in each category's aliases.py (§2 rule 5). |
ir/tir/ |
tir | 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 / …), while specialized statement families such as DispatchCall may live in their own file; 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 Rule 1c. |
parser/ |
parser | 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. |
analysis/ |
analysis | Fact layer over typed HIR: one module per analysis family. The compact public surface lives in analysis/__init__.py; per-target Facts projections live with their owning Target. |
passes/ |
passes | Pass framework (pass_base.py / pass_manager.py) plus concrete transforms (transforms/<pass_name>.py, §2 rule 6). |
target/ |
target | Compilation Target classes, class registration, service selection, and architecture/device facts: base.py owns Architecture / Device / Target / register_target / registered_targets; services.py owns the immutable service descriptors; each backend owns its concrete Target. Authored code constructs Target values; there is no string resolver. |
target/cpu.py |
target | The CpuTarget backend and its CPU code-generation service selection. |
target/hardware/ |
target | The installed hardware database and its generic machinery: the authored Architecture / Device documents, the envelope and evidence-leaf loader, HardwareSpecRegistry, and the exact-key schema reader. It fixes the envelope only; the fact namespace below facts belongs to the target package named by a document's schema. |
target/<backend>/spec.py |
target | One backend's typed hardware schemas and the documents it installs, registered into the shared registry as an import side effect. This is where a fact path, its unit, and its cross-field invariants are validated, and where a document becomes an immutable Architecture / Device value. |
analysis/api.py |
analysis | 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 | 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 | 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 | The per-call AnalyzeContext, carrying the shared root/current lexical Scope while a family traverses its work. |
analysis/scope.py |
analysis | 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 | 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 | Target-independent authored-loop access images, buffer-view folding, and deduplicated versus repeated byte readings. Requires no separate time map. |
analysis/report.py |
analysis | 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 | 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 | 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 | 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 | The compute-cost family: logical flops per DType and bytes per storage level, from the authored program alone. |
analysis/memory.py |
analysis | 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 | 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 | 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. |
visitor_registry/ |
visitor-registry | 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 | 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 | 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. |
target/<backend>/facts.py |
target | One backend's Facts projections selected by its Target's get_facts. They restate installed documents in the shape a family declared and measure nothing. |
target/facts.py |
target | Facts used across consumer families, such as topology limits, plus validation for values returned by Target.get_facts: the requested frozen-dataclass shape and returned type. A Fact used by one family stays with that family; this module holds no projection registry. |
codegen/ |
codegen | Code generation: the immutable CodeGenerator service, linkable / linked products and linker, and concrete generators under <target>/ (mirroring ir/tir/ file layout — tir/<category>/<name>.py emitter, §2 rule 2). Not under ir/ — codegen is a consumer of IR; templates/ holds boilerplate only. |
codegen/registry.py |
codegen | The compatibility re-export of the immutable CodeGenerator descriptor and source-order grouping by equal Target values. It rejects multiple unequal CUDA Target groups before emission and holds no emitter registry. |
runtime/ |
runtime | Runtime support (per-target headers, function templates, launch helpers). |
inspection/ |
inspection | IR visualisation: DOT, Python printer, web viewer. |
dump/ |
inspection | Dump flags, dynamically scoped dump contexts, and file/null dump sinks used by inspection and test integration. |
dsl/ |
parser (authoring namespace) | User-facing import surface: tf/ (HIR namespace) / T/ (TIR namespace, including _platforms.py) / _namespace.py / _stub_gen.py / storage.py / __main__.py. The tf/__init__.pyi and T/__init__.pyi stubs are produced by python -m tilefoundry.dsl regen and are gitignored. |
compile.py |
architecture | tilefoundry.lower / build / compile / jit top-level public verbs. |
module.py |
parser | The @module decorator entry point and module-level topology authoring constants. |
script.py |
parser | @func / @prim_func / @intrinsic decorator entry points. |
__init__.py |
inspection | The top-level view convenience entry and re-exports of public compiler surfaces. |
utils/ |
code-organization | Shared leaf machinery: a module here MUST import nothing from ir/, parser/, passes/, codegen/, runtime/ or cli/, and MUST name no layer. It is depended on and depends on nothing, which is what lets a consumer outside the package — a pre-commit hook under an interpreter with nothing installed — load one of these modules by path and get the same implementation the package uses. A helper that needs to know a layer belongs in that layer; this is not a home for anything that did not fit. |
Stage boundary. The pipeline picture in
architecture §1 places parser/
and codegen/ outside ir/ (front-end producer and back-end consumer); the
physical directory layout reflects that boundary directly.
Reading notes:
ir/holds the IR proper and its sublayers only.ir/types/is the root of the type system;ir/types/shard/is its shard / layout sublayer (architecture §3). The physical nesting reflects the spec's conceptual "sublayer".- The placement of
shard/undertypes/is a filing decision, not a consumer restriction:Topology/Mesh/Layout/ShardLayoutare consumed directly byparser,tir, andcodegen. The hierarchy expresses "role in the type system", not "who may import it". codegen/andparser/sit outsideir/. By the architecture §1 pipeline they are the front-end producer and back-end consumer of IR, not IR sublayers.analysis/sits outsideir/because it derives facts about typed HIR rather than defining an IR layer. It reads the IR and theTargetand decides nothing over what it measures (architecture §5).codegen/<target>/consumes only TIR. The subtree mirrorsir/tir/:prim_functionlives intir/, Stmt emitters intir/stmts/, andmemory//nn//arith//reduce//tensor/each have their own subdirectory. There is nocodegen/<target>/hir/.- Authored launch attributes belong to
ir/tir/launch.py; launch-geometry derivation (grid / block extents) is an internalcodegen/cuda/emit.pyhelper (_derive_launch_config), consumed within codegen itself rather than carried past it as a runtime-owned metadata type. The two launch contracts are distinct even though both are consumed across the codegen boundary.
ir/constraints/, visitor_registry/, and dump/ are cross-cutting packages;
their stable responsibilities are owned by parser,
visitor-registry, and inspection,
respectively. Their internal file layout is not a per-Op contract.
2. File naming and content rules¶
Rule 1 — one real Op = one file. A real Op class lives in
ir/<hir|tir>/<category>/<op_name>.py. The file name is the
snake_case of the Op class CamelCase (MatMul → matmul.py,
RMSNorm → rms_norm.py). TIR effect Ops and TIR-owned Expr Ops
follow the same rule.
Rule 1a — surface-alias schemas have no per-name file. A surface
alias (core-ir §2.3)
has no IR class — its builder routes to a kinded target Op. All
aliases for a category live together in aliases.py (e.g. the 19
HIR math sugar names add / sub / cmp_eq / neg / … all
register in ir/hir/math/aliases.py).
Rule 1b — tag-dispatched IR classes. Binary / Unary /
Reduce and other Op classes that fold many surface names through a
kind attribute live in one file per IR class
(ir/hir/math/binary.py / ir/hir/math/unary.py /
ir/tir/arith.py / ir/tir/reduce.py). This does not contradict
Rule 1: "one Op = one file" means one IR class per file; aliases
are not IR classes, so they go through Rule 1a.
Rule 1c — target-specific IR nodes nest under the dialect. IR is
dialect-first: its primary organizing axis is the dialect, and most
nodes are target-neutral. A node or descriptor that is specific to one
compilation target nests as ir/{dialect}/{target}/{category}/<name>.py;
target-neutral abstractions stay at ir/{dialect}/{category}/. For
example the whole MMA surface is target-owned — the Mma op, the
MmaOpSpec / MmaAtom descriptors, the CUDA SM80 instruction spec, and its
fragment layouts all live under ir/tir/cuda/nn/ (mma.py + mma_atom.py).
The backend-bound construction stays in TIR: HIR is the checking reference
side, and carrying the instruction name in that reference would make two GPU
targets require different HIR references. (codegen/ and runtime/ are
target-first instead — their primary axis is the target — so each tree is
organized by its own primary axis.)
Rule 2 — one (node, target) codegen = one file. Each handler
lives at codegen/<target>/tir/<category>/<name>.py. Stmt emitters,
Expr-Op emitters, and tag-dispatched (arith, reduce) emitters
each get their own file. Codegen consumes TIR only.
Rule 3 — what an IR-class file contains:
- HIR Op file (
ir/hir/<cat>/<name>.py): Op class +@register_typeinfer(Op)+@register_cost_evaluator(Op)(if any). - TIR effect Op file (
ir/tir/<cat>/<name>.py): Op class +@register_typeinfer(Op)(returningUnitType) +@register_verify_stmt(Op). The verify rule keys on the Op class even though the invocation is anEvaluate(op, args)Stmt — see visitor-registry §5. - TIR-owned Expr Op file (
ir/tir/memory/{alloc_tensor,ptr_of,memory_span,tensor_view}.py, …): Op class +@register_typeinfer(Op)+@register_cost_evaluator(Op)(if any). Call-position constraints are checked by the enclosing Stmt's@register_verify_stmt. <category>/aliases.pyfile (Rule 1a):@register_alias(...)declarations whose builders construct the target Op instance.
Rule 4 — what a target codegen file contains: the
@register_codegen_<target> for that (op / stmt) pair, and nothing else.
Rule 5 — <category>/__init__.py re-export rules: real Op submodules are
re-exported; aliases are imported only for registration side effects; user imports
go through parser §2.
Rule 6 — one pass = one file. A pass class lives in
passes/transforms/<pass_name>.py; internal visitors / mutators stay in that file.
Rule 7 — what template files contain. codegen/<target>/templates/*.j2
carry boilerplate assembly only; emitters live in Python walkers.
3. Multi-agent parallelism guarantee¶
The lock granularity is a single (node, target) pair. The naming rules in
§2 imply that two agents working on different
(node, target) pairs touch disjoint files; cross-cutting changes confine
themselves to the owning directory.
4. DSL package layout¶
The author-facing surface is delivered as a namespace package. The two
sub-packages (tf and T) use the OpSchema registry for their corresponding
dialect and return an Op class or alias builder; unknown names raise AttributeError.
4.1 Built-in op-class location convention¶
For @register_op to auto-derive dialect + category, an Op class MUST live
under src/tilefoundry/ir/<hir|tir>/<category>/<file>.py, with its module name
matching that path. Outside it the decorator requires explicit values.
4.2 .pyi stub regeneration¶
The .pyi stubs reflect registered schemas only. After adding a new
@register_op / @register_alias, regenerate them with
python -m tilefoundry.dsl regen.
5. DSL import surface¶
The author-facing exports route through tilefoundry.dsl:
# example
# Canonical authoring imports.
from tilefoundry import func, prim_func
from tilefoundry.dsl import tf, T, Tensor
Tensoris the parser-owned DSL authoring-surface annotation sugar; it is owned bytilefoundry.dsl(defined undertilefoundry.dsl._tensor, re-exported astilefoundry.dsl.Tensor). It is not the IR tensor type — the IR type carrier istilefoundry.ir.types.TensorType. See parser §2.1 for the annotation grammar.DTypeis not re-exported. dtype values use string form in DSL source (Tensor[(8,), "bf16"],zeros((1, 64), "bf16", ...)); the parser converts strings toDType.<name>at attribute-binding time when the receivingParamDefdeclaresannotation=DType.- For users who prefer bare Op names (
add(...)/relu(...)),from tilefoundry.dsl.tf import *binds every registered HIR name into the call site's lexical scope. Without that import the parser requires the namespace formtf.add(...).
The tilefoundry.dsl.{tf, T} modules expose __all__ via their lazy
__getattr__, so a star-import sees every name registered against
the corresponding dialect, including custom Ops registered after
the DSL package first loaded.