Skip to content

fix: throw a descriptive error when iterating a sparse ResizableArray - #323

Open
bvdmitri wants to merge 1 commit into
mainfrom
fix/308-resizable-iterate-error
Open

bvdmitri wants to merge 1 commit into
mainfrom
fix/308-resizable-iterate-error

Conversation

@bvdmitri

Copy link
Copy Markdown
Member

Closes #308. Replaces #317, which was closed — see that discussion.

Problem

Base.iterate for ResizableArray indexed by the reported (max-extent) size with no
isassigned guard, so iterating a ragged or sparsely filled array raised a bare BoundsError
against an internal vector — no mention of the real problem, no hint at the alternative.

Why not just skip the unassigned slots

That was the approach in #317, and it is unsafe. ResizableArray <: AbstractArray{T, N} and
there is no Base.length / Base.IteratorSize override, so length(a) == prod(size(a)) — the
max extent, not the assigned count. (__length, which does count assigned elements, exists at
src/resizable_array.jl:196 but is never used.) collect and friends therefore preallocate
prod(size) and fill by iteration; if iteration ends early, the tail is uninitialized:

skipping approach:  collect(a)     →  [1.0 2.0; 3.0 2.2379712666e-314]
                    map(x->2x, a)  →  [2.0 4.0; 6.0 0.0]
                    a .+ 1         →  BoundsError  (still throws anyway)

Silently returning garbage is strictly worse than crashing for a package that must build the same
factor graph deterministically. Making skipping correct would mean changing length/IteratorSize
to disagree with size, which is awkward while the type is an AbstractArray — out of scope here.

Change

iterate guards with isassigned and raises a descriptive error instead:

Cannot iterate over `ResizableArray` at index (2, 2) because this slot is unassigned.
Ragged or sparsely filled arrays cannot be iterated densely, use `GraphPPL.vec(array)`
to iterate over the assigned elements only.

vec already guards with isassigned and is the supported traversal for ragged arrays; the
existing "sparse ResizableArray" testset already documented that intent.

Also guards the empty case in the 0-argument method, which previously destructured
iterate(CartesianIndices(...)) unconditionally.

Dense iteration order and the zip/enumerate protocol are unchanged.

Tests

Added to the existing "iterate" test item in test/resizable_array_tests.jl: a ragged 2×2 array
where collect, map, a comprehension and a for loop each throw the new message, vec still
returns the assigned elements, and empty arrays of rank 1–3 iterate to zero elements.

Verified the new assertions fail on main (8 failures) and pass here. Full suite on Julia 1.12:
258/258 test items, all passing — relevant because GraphPPL uses ResizableArray for vector and
tensor model variables, so this confirms nothing internally relies on iterating ragged arrays.

Known limitation

Broadcasting (a .+ 1) still raises a raw BoundsError, because it goes through getindex rather
than iterate. That is pre-existing and still loud, so it is left alone here.

🤖 Generated with Claude Code

`Base.iterate` indexed by the reported (max-extent) `size` without an
`isassigned` guard, so iterating a ragged or sparsely filled array raised a
bare `BoundsError` pointing at an internal vector, with no hint about the real
problem or the supported alternative.

`iterate` now checks `isassigned` and raises an error naming the unassigned
index and pointing at `GraphPPL.vec`, which already guards with `isassigned`
and is the supported way to traverse ragged arrays.

Note it deliberately does *not* skip unassigned slots. `ResizableArray <:
AbstractArray` and there is no `length`/`IteratorSize` override, so
`length == prod(size)`; skipping would break the iteration protocol and leave
`collect`/`map` results holding uninitialized memory. Failing loudly is the
safer contract for a package that must build factor graphs deterministically.

Also guards the empty case in the 0-argument method, which previously
destructured `iterate(CartesianIndices(...))` unconditionally.

Closes #308.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.70%. Comparing base (4cc7c6c) to head (2e28f29).

Files with missing lines Patch % Lines
src/resizable_array.jl 90.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #323      +/-   ##
==========================================
- Coverage   90.72%   90.70%   -0.02%     
==========================================
  Files          16       16              
  Lines        2263     2270       +7     
==========================================
+ Hits         2053     2059       +6     
- Misses        210      211       +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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ResizableArray iteration throws BoundsError on ragged/sparse tensors

1 participant