Shapes (Spec.Shape) #
Shape is the type-level shape descriptor for tensors.
TorchLean uses shape-indexed tensors:
Tensor α s
so Lean checks shape compatibility before tensor code can run.
Representation #
A shape is a list of dimensions, outermost first, so model and application code writes one as ordinary bracket notation:
[]for a scalar[n]for a vector[m, n]for a matrix
Shape is a reducible abbreviation for Tensor.Internal.Shape, which is List Nat, so the
spec-level shape and the shape carried by a tensor buffer are the same type. There is no conversion
between them and no cast to transport a tensor across the two views.
Shape-recursive definitions and proofs are still written with the two names Shape.scalar and
Shape.dim, which are @[match_pattern] abbreviations for [] and · :: ·. They may be used in
patterns exactly like constructors, and induction/cases on a shape offer the cases scalar and
dim through the eliminators registered below.
Common utilities #
Spec.Shape.size : Shape → Natis the total number of scalar elements (“numel”).Spec.Shape.rank : Shape → Natis the number of axes.
PyTorch analogy:
- a shape itself corresponds to
tensor.shape(a tuple of dimensions). Spec.Shape.rank scorresponds totensor.ndim.Spec.Shape.size scorresponds totensor.numel().
Broadcasting and axes #
Broadcasting is encoded by the decidable proposition CanBroadcastTo and its typeclass wrapper
BroadcastTo. Because the relation is a proposition, tensor operations depend only on the two
shapes and never on how the relation was proved.
This is an intentionally asymmetric relation ("broadcast s1 to s2"), because most tensor code
is naturally written by choosing the output shape and requiring each input to broadcast to it.
The typeclass wrapper BroadcastTo keeps higher-level specs readable: in many cases Lean can infer
the broadcast evidence automatically, so call sites do not have to thread proofs around by hand.
It also defines axis-validity helpers (NonemptyAxis) and a wellFormed predicate for “all
dimensions are positive”, which is useful when you want to rule out degenerate cases in proofs.
Tensor shape descriptor used to index spec-level tensors (TorchLean.Tensor α s).
Use outermost-first bracket notation: [], [n], [m, n], and so on. This is the same type as
the shape carried by a tensor buffer, Tensor.Internal.Shape, and therefore the same type as
List Nat.
Instances For
The shape of a scalar: no axes. Usable in patterns, like a constructor.
Instances For
A shape with outermost axis of extent n over s. Usable in patterns, like a constructor.
Instances For
Recursion on a shape, one axis at a time.
This is the eliminator induction s uses, so the cases are named scalar and dim and the tail
of a dim is again a Shape rather than a List Nat. Ordinary list induction is unaffected: a
hypothesis whose type is spelled List Nat still eliminates to nil and cons.
Instances For
Model code writes shapes as dimension lists. For example, Tensor Float [4, 2] is a four-by-two
tensor of Lean Float values, while Trainer.Dataset [2] [1] describes supervised samples with
two input values and one target value. The expected Shape type directs Lean to elaborate the list
through Shape.ofList.
The brackets are shape notation, not tensor storage: a value of type Tensor Float [4, 2] is still
a Tensor, never a List. During elaboration the notation reduces to the recursive shape below,
preserving the definitional equalities used by tensor programs and proofs. Ordinary model code
should not write the expanded .dim form.
Output length of a floor-mode sliding window with symmetric padding.
For positive kernel and stride with
$\mathtt{kernel}\le\mathtt{input}+2\mathtt{padding}$, this is
$(\mathtt{input}+2\mathtt{padding}-\mathtt{kernel})/\mathtt{stride}+1$. Invalid geometry has
length zero, so saturated natural-number subtraction and division by zero cannot create a phantom
output element.
Instances For
Effective extent of a dilated kernel along one axis.
Instances For
Output length of a dilated sliding window with independent padding on each side.
Invalid geometry has length zero, as in slidingWindowOutDim.
Instances For
Read a dimension list as a shape.
The two types are equal, so this is the identity. It is kept because a great deal of code names it explicitly at the boundary where dimensions arrive as a list.
Instances For
Build a shape from runtime dimensions stored outermost first.
Instances For
View a shape as its dimension list.
The two types are equal, so this is the identity. It is kept because front ends and bridges name it explicitly where a list is the natural spelling.
Instances For
Print a shape using the same dimension-list convention as model code.
Instances For
Swap two adjacent dimensions at a given depth (0‑based from the outermost).
Instances For
Swapping adjacent dims at depth depth twice returns the original shape.
Applying a concatenated list of swaps is applying the two halves in order.
Transposition permutations are built up as lists of adjacent swaps, so this is the lemma that lets a composite permutation be reasoned about one factor at a time.
Replaying adjacent-axis swaps in reverse order restores the original shape.
Append a new innermost dimension.
Instances For
Add a new outermost dimension.
Instances For
Concatenate two shapes, preserving the dimensions of the first shape as leading axes.
Instances For
Decide whether two shapes are equal.
Shape is an abbreviation for List Nat, so equality is decidable by the usual list instance.
Reach for this function rather than if h : s = t when either side is bound by a local let:
instance search on such a binding can get stuck, while a direct call always elaborates.
Instances For
Reversing a concatenation reverses each part and swaps them.
Shape parsers read a shape back to front (innermost axis first), so this is the lemma that turns a
leading ++ [rows, cols] layout into the cols :: rows :: rest pattern they match on. It is stated
about reverse rather than concat so that a simp set naming it leaves the other occurrences of
concat alone. It carries no simp attribute for the same reason: concat is the normal form for
a composed shape, and rewriting every occurrence away would strand the lemmas stated about it.
appendDim multiplies the number of scalar elements by the appended dimension.
This lemma is the standard justification for reshape tricks where we:
prependDim multiplies the number of scalar elements by the new outermost dimension.
This is the counterpart of size_appendDim for the front of a shape, and unlike that lemma it holds
by definition: size already recurses on the outermost axis.
Convert to an array of dimensions (outermost first).
Instances For
Typeclass-friendly broadcasting (BroadcastTo) #
The CanBroadcastTo relation is asymmetric (“broadcast s₁ to s₂”), matching how most
operations are written: we pick a target shape and require each operand to broadcast to it.
The BroadcastTo wrapper lets Lean search for a broadcast proof automatically, which is convenient
for higher-level specs (layers/models) where the broadcasting details are not the point.
PyTorch analogy:
- PyTorch broadcasting aligns shapes from the trailing dimensions by implicitly prepending
1s to the shorter shape. - Our
Shapeis an outermost-first tree, so the corresponding operation isexpand_dims: it inserts leading/outer dimensions to reach the target rank (this is the "prepend1s" step). dim_1_to_ncorresponds to PyTorch's "dimension 1 can expand to n" rule.
The rank is the number of dimensions.
Not a simp lemma: rank is the normal form for a shape's number of axes.
Insert a dimension at an axis, where axis 0 is outermost.
Callers that construct a tensor of this shape also carry a proof that the axis does not exceed the input rank. The out-of-bounds scalar case is therefore unreachable in typed tensor operations.
Instances For
Inserting at axis zero adds a new outermost dimension.
Prepending one dimension increases the rank by one.
A shape decomposed into a leading prefix and a suffix of a prescribed rank.
- leading : Shape
Axes preceding the suffix.
Extents of the suffix axes.
The suffix has the requested number of axes.
The decomposition reconstructs the original shape.
Instances For
Split the final suffixRank axes from a shape.
Instances For
Replace every dimension by one while preserving the rank of a shape.
Instances For
Collapsing every axis to length one leaves the rank alone.
A shape whose every axis has length one holds exactly one element.
Every shape has the same rank as itself.
The broadcast relation (CanBroadcastTo) #
CanBroadcastTo source target is a proposition on the two shapes, so every tensor operation that
consumes it depends only on the shapes and never on how the relation was proved. It is defined by
recursion on the target shape, which makes it decidable (canBroadcastTo?, decide), and the
structural rules of NumPy and PyTorch are recovered as theorems: CanBroadcastTo.scalar,
CanBroadcastTo.dim_eq, CanBroadcastTo.dim_1_to_n, and CanBroadcastTo.expand_dims.
The equal-dimension rules require equal-rank tails, so every rank difference is resolved by
expand_dims before extents are compared. The internal tensor layer states the same relation on
dimension lists (List.Forall₂ after padding the source with leading ones);
CanBroadcastTo.forall₂_toList in the broadcasting module connects the two forms.
CanBroadcastTo source target holds when source broadcasts to target with right-aligned
axes: after prepending singleton axes to reach the target rank, every source extent equals the
target extent or is one.
Instances For
A scalar broadcasts to a scalar.
Nothing with an axis broadcasts down to a scalar; broadcasting only ever adds extent.
A scalar broadcasts across a new outer axis exactly when it broadcasts to the tail.
The broadcast relation is decidable by the same recursion that defines it.
Decide the broadcast relation at runtime, returning the proof when it holds.
IR passes and dynamic lowerings that only know shapes at runtime use this instead of trusting that declared input and output shapes are compatible.
Instances For
canBroadcastTo? succeeds exactly when the relation holds.
Broadcasting never lowers the rank.
Scalar shapes agree. Higher-rank scalar broadcasts are built with expand_dims.
Matching outer dimensions preserve broadcasting of equal-rank tails.
An outer dimension of length one can expand to any target length.
A new outer target dimension aligns a source of lower rank.
Removing a target axis that only aligns ranks keeps the source broadcastable.
Every shape broadcasts to itself without expanding an axis.
A scalar broadcasts to any shape by inserting every target dimension.
A shape of singleton axes broadcasts to any shape of the same rank.
A suffix broadcasts across an arbitrary collection of newly prepended target dimensions.
Typeclass wrapper for CanBroadcastTo so broadcast proofs can be inferred for literal
shapes.
- proof : s₁.CanBroadcastTo s₂
Instances
Scalar shapes broadcast directly. Leading target dimensions are inferred by expand_dims.
Broadcasting preserves equal leading dimensions when the tails broadcast.
Dimension 1 can broadcast to any n (PyTorch's main broadcast rule).
Prepend an outer dimension (the "expand_dims" step used to align ranks).
Axis permutation that exchanges axis₁ and axis₂ and fixes every other axis.
Instances For
Remove one axis from a shape. Invalid axes leave the shape unchanged.
Instances For
Replace the final axis extent. A scalar shape is left unchanged.
Instances For
Evidence-directed final-axis replacement agrees with ordinary shape replacement.
Replacing an in-bounds axis preserves rank.
Axis evidence #
Axes are zero-based natural numbers. AxisInBounds axis s says only that the axis exists;
HasNonemptyAxis axis s additionally says that its extent is positive. Shape-preserving operations
such as softmax need the first condition. Reductions whose definition selects an element, such as
maximum and minimum, use the second.
Negative axes are normalized by frontends before reaching this layer. The innermost axis of a
positive-rank shape is s.rank - 1.
Convert an ordinary rank proof into the evidence consumed by tensor operations.
Extent of a statically valid axis.
Instances For
The outermost dimension is axis zero, regardless of its extent.
An inner axis remains in bounds under an additional outer dimension.
Looking through an outer dimension preserves the extent of an inner axis.
Decide whether a natural number names a dimension of s, returning typed evidence.
Instances For
The decision procedure succeeds whenever static in-bounds evidence is available.
NonemptyAxis axis s says that axis selects a positive-length dimension of s.
The constructors follow the recursive representation of Shape, so reduction definitions can
eliminate this evidence while recursing through outer dimensions.
- zero {n : ℕ} {s : Shape} : NonemptyAxis 0 (dim (n + 1) s)
- succ {n : ℕ} {s : Shape} {axis : ℕ} : NonemptyAxis axis s → NonemptyAxis (axis + 1) (dim n s)
Instances For
A nonempty axis is, in particular, a valid axis.
Axis zero is nonempty when the outer dimension is positive.
Nonemptiness of an inner axis is unchanged by an outer dimension.
Return evidence that axis addresses a positive dimension of s.
Executable consumers use this to recover the proposition required by typed tensor operations from
a raw runtime axis. Invalid axes, including axes into zero-sized dimensions, return none.
Instances For
Typeclass wrapper used when nonempty-axis evidence can be inferred statically.
- proof : NonemptyAxis axis s
The selected dimension has positive extent.
Instances
Instance: axis 0 is valid for any positive outer dimension.
Package a proof that the outer dimension is nonzero as axis evidence.
Package positivity of the outer dimension as axis evidence.
Nonemptiness of an inner axis is unchanged by an outer dimension.
Well-formedness (wellFormed) #
well_formed s means "all dimensions are positive".
Why this matters (and why we designed it this way):
- Many definitions use
Fin nindexing; ifn = 0, there is no index and you end up with either vacuous truths or extra cases that obscure the intent of the lemma. - Some common ops become awkward or partial at
n = 0. For example, a mean typically divides by the number of elements, son = 0needs special-case semantics. - PyTorch does allow zero-sized dimensions, and most ops define a sensible result for them. We intentionally keep that complexity out of the core spec layer because it makes proofs much more case-heavy. When we need zero-dimension tensors, we introduce them with explicit semantics instead of relying on incidental behavior.
This is a pragmatic choice: proofs and specs are shorter, and runtime checks can still handle edge cases separately.
well_formed s means "all dimensions of s are positive" (recursively).
Instances For
Size positivity #
If all dimensions of a shape are positive, then the total number of scalar elements is positive.
This is a small but useful bridge lemma: many reductions are only defined for nonempty dimensions,
and WellFormed is our standard way of expressing that assumption.
If s.wellFormed, then Spec.Shape.size s > 0.
A shape of positive total size has no zero dimension.
Every in-bounds axis of a well-formed shape has positive extent.
Package a valid axis of a well-formed shape as inferred reduction-axis evidence.
Typeclass wrapper for Shape.wellFormed.
We use a typeclass (instead of passing a wellFormed proof everywhere) because it mirrors how
other "side conditions" are handled in the library: call sites stay clean, and instances can be
provided locally (e.g. letI : Shape.WellFormed s := ...) when needed.
- proof : s.wellFormed
Instances
Adding a nonzero dimension preserves well-formedness.
Infer nonemptiness of any valid axis from WellFormed s.
padLeft n s prepends n singleton dimensions to a shape.
PyTorch analogy: unsqueeze(0) repeated n times (or equivalently viewing a tensor as having
extra leading dimensions of size 1). This is also the "prepend 1s" step you see in broadcasting.
Prepend n leading singleton dimensions (size 1) to a shape.
Instances For
The list view of a padded shape prepends n ones.