named-indices — axis names the compiler checks¶
What it shows. In NumPy, PyTorch and JAX a tensor's axes are numbered, so
a @ b type-checks whenever the inner lengths happen to agree. Transpose a
weight matrix by accident, swap a batch axis for a sequence axis, and you find
out at runtime — if you are lucky.
In Tlaloc an axis carries a name in its Kotlin type:
DTensor<Rank2<Named<Batch, Sym>, Named<SeqLen, Sym>>, F32>
// ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
// axis 0 is "batch" axis 1 is "seq"
contract is declared so the contracted axes must share a name, which turns a
shape bug into an overload-resolution failure in Kotlin's own type checker
— a red squiggle in the IDE, no plugin diagnostic and no runtime check
involved.
The example does two contractions and points at a third that does not compile:
- rank-2 —
(Batch × SeqLen) · (SeqLen × Hidden) → (Batch × Hidden). - rank-4 — the
QKᵀcore of attention, with two batching axes (Batch,Heads) and one contracting axis (Dim). This is the real transformer shape; nothing is reshaped down to 2-D first. - the compile error — a real source file that contracts a
SeqLenaxis against aVocabaxis, and one command that makes the compiler reject it in front of you, before a single float is allocated.
The program that must not compile¶
src/shapeError/kotlin/ShapeError.kt is a
real file in a real source set — not a comment you have to uncomment. It contracts
a (Batch × SeqLen) tensor with a (Vocab × Hidden) one. Watch the type checker
refuse it:
e: ShapeError.kt:35:24 Argument type mismatch: actual type is
'DTensor<Rank2<Named<Vocab, Sym>, Named<Hidden, Sym>>, F32>', but
'DTensor<Rank2<Named<SeqLen, Sym>, Named<NameN, N>>, F32>' was expected.
(abridged: the compiler also spells out the two type parameters it could not infer, and prints the path in full.)
SeqLen is not Vocab, so no overload of contract matches and the call does
not resolve. That is Kotlin's own type checker — this project does not put
the Tlaloc compiler plugin on its classpath at all. The rule is expressed in
ordinary generics, which is why your IDE draws the squiggle with no Tlaloc-specific
tooling installed.
The task is wired to fail the build if that compile ever succeeds. It is not part
of run, build or check, so the example itself stays green.
Running it¶
This is a standalone Gradle project. It resolves Tlaloc from mavenLocal, exactly as your own project would, so publish first:
Expected output¶
Verbatim, from this machine (GB10, JDK 25, Kotlin 2.4.20):
[1] rank-2 contraction (Batch x SeqLen) . (SeqLen x Hidden) -> (Batch x Hidden)
dims: [2, 4] (expected [2, 4])
[0] 3.800 4.400 5.000 5.600
[1] 8.300 9.800 11.300 12.800
[2] rank-4 attention core Q(B,H,Tq,D) . Kt(B,H,D,Tk) -> S(B,H,Tq,Tk)
dims: [1, 2, 4, 4] (expected [1, 2, 4, 4])
--- head 0 ---
q=0 0.1436 0.1504 0.1572 0.1641
q=1 0.3779 0.4004 0.4229 0.4453
q=2 0.6123 0.6504 0.6885 0.7266
q=3 0.8467 0.9004 0.9541 1.0078
--- head 1 ---
q=0 3.2998 3.3691 3.4385 3.5078
q=1 4.0342 4.1191 4.2041 4.2891
q=2 4.7686 4.8691 4.9697 5.0703
q=3 5.5029 5.6191 5.7354 5.8516
[3] the program that does NOT compile
fun shapeErrorThatMustNotCompile(
a: DTensor<Rank2<Named<Batch, Sym>, Named<SeqLen, Sym>>, F32>,
b: DTensor<Rank2<Named<Vocab, Sym>, Named<Hidden, Sym>>, F32>,
) {
// Contract a (Batch x SeqLen) with b (Vocab x Hidden): SeqLen != Vocab.
val c = a contract b
println(c.dims.toList())
}
`a`'s axis 1 is named SeqLen; `b`'s axis 0 is named Vocab. No overload
of `contract` matches operands sharing no axis name, so the call does
not resolve. Watch it:
./gradlew -p examples/named-indices shapeError
e: ShapeError.kt:35:24 Argument type mismatch: actual type is
'DTensor<Rank2<Named<Vocab, Sym>, Named<Hidden, Sym>>, F32>', but
'DTensor<Rank2<Named<SeqLen, Sym>, ...>>, F32>' was expected.
That is KOTLIN'S OWN type checker — this project does not even put the
Tlaloc compiler plugin on its classpath. The rule lives in ordinary
generics, so your IDE draws the squiggle with no Tlaloc tooling at all.
named-indices OK
What to read in the source¶
All of it is in src/main/kotlin/Main.kt:
| Look at | For |
|---|---|
rank2Contraction() |
How a named tensor is built, and which axis survives a contraction |
object KeySeqLen : IndexName |
Defining your own axis name — three lines |
attentionScores(...) |
The rank-4 signature: two batching axes, one contracting axis, spelled in types |
src/shapeError/kotlin/ShapeError.kt |
The program that is supposed to fail — ./gradlew -p examples/named-indices shapeError |
Notes¶
- No compiler plugin here. This example depends on
io.github.pedronahum:tlaloc-coreonly. Named axes are enforced by Kotlin generics, so the K2 plugin is not in the loop — its job is loweringgrad { }bodies (see../quickstart/). That separation was the design goal, not a limitation; seedocs/audits/named_indices_audit.md. - When the rank-4 contraction lowers to StableHLO it becomes a
stablehlo.dot_generalwhoselhs_batching_dims = [0, 1]andlhs_contracting_dims = [3]are derived from the names, not hand-written.