Skip to content

Check axis lengths, interpolate them with $, and export the macros (v0.3.0) - #62

Merged
AntonOresten merged 4 commits into
mainfrom
axis-lengths
Oct 23, 2026
Merged

AntonOresten merged 4 commits into
mainfrom
axis-lengths

Conversation

@AntonOresten

@AntonOresten AntonOresten commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Lengths are checked. Every length the left side of a pattern fixes is now compared with the input before anything reshapes it. That covers integers, 1, and names given a length by keyword. Before, "4 b -> b" reduced an axis of length 3, and a=4 for an ungrouped a was never looked at, because a known length only entered the plan inside a group, as a split. A group with one name left to infer must be divisible by the rest. Errors name the input axis, also past an ellipsis:

julia> @reduce(sum, rand(3, 2), "4 b -> b")
ERROR: DimensionMismatch: Input axis 1 has length 3, but the pattern expects 4

"1 b -> b" on a length-3 axis still throws. That is the same rule, now with the same message.

1 inside parentheses is a unit, as in Python einops: (b a 1) is (b a), and () or (1) is a length-one axis. An entry the plan builders do not recognize now raises instead of being skipped, which is what turned (b a 1) into an empty reshape (#60). Other integers say where they are allowed: reduced on the left of reduce, or created on the right of repeat.

$k interpolates a length (#35): an axis of length k, as if the value were written there, the way Python einops users write f"({k} a)". Like an integer other than 1, it can only be reduced or created, and each occurrence is an axis of its own:

@reduce(mean, x, "(h $k) (w $k) c -> h w c")   # k×k pooling
@repeat(x, "L1 L2 -> ($k L1) ($k L2)")         # #36's use case

The value never enters the pattern's type (#34):

  • The shorthand macros pass it on as a hidden keyword.
  • einops"..." returns a new public BoundPattern, which shows as the pattern with the values in place.

A length computed at run time therefore leaves the call type stable, and @inferred tests cover this. An integer built into a --> tuple pattern still cannot do that, since the type can't tell 1 from 3; the --> docstring now says so and points to the alternatives. For the same reason a value of 1 stays an axis of length one rather than becoming a unit: reduced or created, that gives the same result, but $k is rejected in rearrange whatever its value.

A name is an index, so duplicate names stay rejected. #36 asked for duplicates as a size hint, but an index twice on one side means a diagonal, which is einsum's business ("k k ->" is the trace, there and in Python). $k covers the size-hint use case instead. Keeping an axis through rearrange takes a name and a keyword, "(h d) n -> d n h"; h. einsum rejects $: such an axis could only be summed out, which a name does too, and einsum isn't type stable to begin with.

Version 0.3.0 requires Julia 1.10 and exports @rearrange, @reshape, @reduce, @repeat and @einsum, which also makes the README quickstart work after using Einops. Breaking for:

  • calls that passed a wrong length, which used to give a wrong result silently;
  • code loading TensorCast.jl (which exports @reduce) or Einsum.jl (which exports @einsum) alongside Einops; those names now need qualifying.

Tests: test/axis_lengths.jl is new. The full suite passes on Julia 1.10 and 1.13. Zygote passes on 1.12 and Reactant on 1.13, each with new tests for checked and interpolated lengths, including a wrong length throwing while tracing. Doctests and the docs build are clean.

Closes #34, closes #35, closes #36, closes #60.

🤖 Generated with Claude Code

AntonOresten and others added 3 commits October 23, 2026 16:07
…unit

`"4 b -> b"` reduced an axis of length 3, and `a=4` for an ungrouped `a` was never
compared with the input: a known length only entered the plan inside a group, as a
split. Every length the left side fixes, from an integer, a unit or a keyword, is now
checked before anything reshapes, with the input axis and the expected length in
the message. A group with one name left to infer must be divisible by the rest.

`1` inside parentheses is a unit, as in Python einops, so `(b a 1)` is `(b a)`
(#60). An entry the plan builders do not recognize now raises instead of being
skipped, which is what turned `(b a 1)` into an empty reshape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ern's type

`$x` in a pattern is an axis of length `x`, named after the expression, so the
same expression twice is the same axis: `"($h d) n -> d n $h"` splits off `h`
and keeps it. The shorthand macros pass the value on as a keyword, and
`einops"..."` returns a `BoundPattern` carrying the values beside the pattern,
which every op and `einsum` accept. Either way the pattern's type holds only the
name, so a length computed at run time leaves the call type stable, which an
integer built into a tuple pattern cannot (#34, #35).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`@rearrange`, `@reshape`, `@reduce`, `@repeat` and `@einsum` become the
exported interface, so the README's quickstart works after `using Einops`.
Lengths that were passed but wrong now throw, which also makes this a minor bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.32432% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.49%. Comparing base (ff0f1e8) to head (7eff20c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/repeat.jl 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #62      +/-   ##
==========================================
- Coverage   99.56%   99.49%   -0.07%     
==========================================
  Files          15       15              
  Lines         683      793     +110     
==========================================
+ Hits          680      789     +109     
- Misses          3        4       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…ength

A name is an index, and an index twice on one side is a diagonal, which is
einsum's business. `$k` named its axis after the expression, which mixed the
two: `$k` and `$h` were different axes at equal values, and `$k $k` was a
duplicate. Now `$k` is what the value would be if written there, an axis of
that length that can only be reduced or created, each occurrence its own. So
`"(h $k) (w $k) -> h w"` pools and `"a b -> ($k a) ($k b)"` repeats, without
duplicate names (#36). Keeping an axis through `rearrange` takes a name and a
keyword, `"(h d) n -> d n h"; h`. `einsum` takes no `$`: such an axis could only
be summed out, which a name does too. The value still rides beside the
pattern, so the type stays stable; only a value of 1 differs from writing 1,
staying an axis of length one rather than a unit, since the value cannot
decide the plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AntonOresten
AntonOresten merged commit 1c8d0d7 into main Oct 23, 2026
4 of 7 checks passed
@AntonOresten
AntonOresten deleted the axis-lengths branch October 29, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant