Skip to content

Make out-of-bounds panics report the caller's location - #325

Open
hxperl wants to merge 1 commit into
bluss:masterfrom
hxperl:track-caller-oob
Open

hxperl wants to merge 1 commit into
bluss:masterfrom
hxperl:track-caller-oob

Conversation

@hxperl

@hxperl hxperl commented Sep 15, 2026

Copy link
Copy Markdown

ArrayVec::insert is annotated #[track_caller], but only one of its two documented panics honours it.

Its doc says "Panics if the array is full or the index is out of bounds." The full-array panic comes from the .unwrap() in insert's own body, so it reports the caller. The out-of-bounds panic comes from panic_oob! inside try_insert, which is not annotated — so it reports a line inside arrayvec. Same function, same doc sentence, two different behaviours. remove and swap_remove were never annotated at all.

#236 added #[track_caller] for the capacity-overflow panics — its title is literally "track_caller for capacity overflow panics" — and #212 was then closed as fixed by it. The panic_oob! family was left behind.

Reproduction

Against master (8a7c821), capturing PanicHookInfo::location() via a panic hook:

test file            = tests/repro.rs
insert  (full)       -> tests/repro.rs:28        <- caller, correct
insert  (oob)        -> src/arrayvec.rs:311      <- arrayvec internals
remove  (oob)        -> src/arrayvec.rs:423
swap_remove (oob)    -> src/arrayvec.rs:375
try_insert (oob)     -> src/arrayvec.rs:311

With this branch, all five report tests/repro.rs.

The change

#[track_caller] on try_insert, remove and swap_remove.

remove and swap_remove raised their panic from inside an unwrap_or_else closure. Closures do not inherit #[track_caller], so annotating the enclosing function alone would not have worked; both are rewritten as a match that panics directly in the annotated body. That is the only reason those two hunks are larger than one line.

I also added the missing "if the" to try_insert's panic doc line (***Panics*** \index` is out of bounds.`), since it is the same sentence. Happy to drop that hunk if you would rather keep this to the attribute.

Performance

You raised this on #212 in 2022 — "Needs knowledge about the performance impact ... for other hotter methods" — so I measured it rather than assert it.

Wall-clock benchmarking was useless on my machine (shared, ±100% noise), so I compared generated code instead. Release build, aarch64-apple-darwin, opt-level=3, three #[no_mangle] probe functions calling each method on an ArrayVec<u32, 64>:

function before after diff
try_insert 73 instrs 73 instrs identical except the anon symbol number for the panic Location
remove 75 instrs 74 instrs two stores merged into one stp
swap_remove 57 instrs 55 instrs one redundant mov w8, w8 dropped

There is no cost because these methods are generic over CAP and get monomorphised and inlined at the call site, where the implicit caller-location argument is constant-folded away; the &Location only materialises on the cold panic path, which was already constructing one. The remove/swap_remove shrink comes from dropping the closure, not from the attribute.

Caveat: that measurement is one target and one opt-level, with the calls inlined. I have not checked a forced #[inline(never)] call, where the extra pointer argument would be real. If you want the probe crate or numbers on x86-64, say so.

Testing

cargo test --all-features and cargo test --release --all-features both pass on macOS aarch64 (53 + 45 doctests + the integration suites), and cargo build --no-default-features builds. The new test in tests/tests.rs fails on master and passes here. I could not check the 1.51 MSRV job locally — no toolchain on this machine and not enough disk to install one — but #[track_caller] is stable since 1.46 and is already used in the crate, so I expect it to be fine; CI will confirm.

Not included

ArrayString::remove (panic!("cannot remove a char from the end of a string")) and ArrayString::truncate (assert!(self.is_char_boundary(..))) have the same gap, while ArrayString::push/push_str are annotated. I left them out to keep this to one defect — say the word and I will add them here or in a separate PR.

Closes the part of #212 that #236 did not cover.


Written with assistance from Claude Code (Claude Opus 5). All reproduction output, benchmark figures and test results above are real runs on my machine, not generated text.

`insert` is annotated `#[track_caller]`, but it raises its out-of-bounds
panic from inside `try_insert`, which is not annotated. The annotation is
therefore defeated on that path: a full-vector `insert` blames the caller,
while an out-of-bounds `insert` blames src/arrayvec.rs. `remove` and
`swap_remove` were never annotated either.

bluss#236 added `#[track_caller]` for the capacity-overflow panics and bluss#212 was
closed as fixed by it, but the `panic_oob!` family was left behind.

Annotate `try_insert`, `remove` and `swap_remove`. `remove` and
`swap_remove` raised their panic inside an `unwrap_or_else` closure, and
closures do not inherit `#[track_caller]`, so both are rewritten as a
`match` that panics directly in the annotated function body.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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.

1 participant