Skip to content

Latest commit

 

History

History
178 lines (126 loc) · 12.9 KB

File metadata and controls

178 lines (126 loc) · 12.9 KB

Arraylist Library Implementation Review (array.h)

Date: 2026
Scope: Complete static + dynamic review of the core implementation in array.h (header-only C11/C17 dynamic arrays, slices, spans) plus supporting docs and tests.
Reviewer: Grok (following approved plan in session 019e84eb-...)
Result: No behavioral bugs found in the library. The implementation is defensively written and holds up under significantly increased test pressure. The primary gaps were in coverage of documented contracts, not in the code itself.


Executive Summary

  • Static audit of every macro, overflow helper, allocation path, slice/span validity, and macro side-effect sequencing completed against the contracts in docs/*.md, README, and header comments.
  • Dynamic verification:
    • Existing tests/compile/ suite (now 9 files) passes cleanly under gcc + clang, strict C11 and GNU modes.
    • New tests (growth_edges.c, realloc_preserve.c) added for growth policy, non-power-of-2 capacities, and slice durability across reallocs.
    • Multiple ASan+UBSan+O2/LTO runs on the hottest paths (growth, reserve, slices after block moves, failure returns) — zero reports.
    • Strict aliasing warnings (-Wstrict-aliasing=3) + UBSan on LTO+O3 builds: no diagnostics or runtime violations triggered.
  • Candidate issues investigated (from the plan):
    1. Array_Header type punning / strict aliasing — theoretical concern only; no observable problem on gcc/clang even under LTO+UBSan.
    2. (void**) out-ptr casts — same; portable in practice on all tested ABIs.
    3. Realloc-failure "preserve original state" contract — verified by code inspection of the exact success-path assignment + new coverage; full runtime injection harness prepared (interposer sources) but not required to surface a bug. 4–6. Growth near limits, slice-after-move arithmetic, max-size edges — exercised by new growth_edges.c + sanitizer runs; all correct.
  • Overall grade for the library implementation: A / A+ (excellent defensive coding; only real "bug" was missing test surface for some documented promises).

The library's careful overflow checks, short-circuit guards in macros, and "failures never discard data" discipline are real strengths.


What Was Reviewed

Files & Artifacts

  • array.h (primary, ~600 lines) — every public macro, every static inline helper, layout calculations, FAM usage.
  • All 4 canonical docs (docs/overview.md, api-reference.md, quickstart.md, examples.md) + root README — every precondition, failure mode, and complexity claim cross-checked.
  • tests/compile/ harness + all 8 original .c files.
  • Visual models in site/src/components/visuals/ (GrowthVisualizer growth loop, SliceVisualizer validity rules, etc.) — behavioral fidelity confirmed (minor demo simplifications only).
  • New artifacts: growth_edges.c, realloc_preserve.c, realloc_interposer.c (and .so build).

Key Properties Verified

  • Overflow arithmetic in all four helpers is correct and used consistently before every malloc/realloc size calculation.
  • array_grow_capacity correctly implements "double from current until >= min, with safe fallback near SIZE_MAX".
  • array_try_push never performs the element assignment or count increment unless reserve succeeded; the count != SIZE_MAX guard prevents +1 wrap in the argument.
  • Slice (start/count offsets) objects remain valid across reallocs that move the backing storage; only Spans and raw element pointers are invalidated. New test growth_edges.c creates a slice, forces multiple growths that move the block, then successfully re-validates it with both array_try_slice_at_t and array_try_span_t.
  • NULL, zero-capacity, and empty-slice (low == high) cases are handled per the docs.
  • Realloc failure paths in reserve_impl leave the caller's pointer variable and all prior data untouched (assignment to *block only on the success side of the if (new_block) check).
  • Public fields are intentionally exposed; unchecked macros have the documented preconditions and UB on violation.

Candidate Bugs Investigated — Outcomes

Candidate (from plan) Location(s) Investigation Result
Strict aliasing via Array_Header* casts on ArrayStruct(T)* array_reserve_impl, try_*_impl (multiple) -Wstrict-aliasing=3, UBSan, LTO+O3, both gcc & clang, full test matrix No warnings, no runtime violations, no misbehavior. Remains a theoretical portability nit for extremely pedantic or future compilers. Not a bug in practice on current toolchains.
(void**) casts for generic out_ptr array_try_at, array_try_slice_at_t, etc. Same sanitizer + warning matrix Clean. Works because all object pointers have uniform representation on the tested platforms.
Untested "realloc failure preserves data" array_reserve_impl:241-249 + array_try_push Code inspection + growth_edges + sanitizer runs + realloc_preserve.c (with prepared interposer) Contract holds in source. No counter-example found. The interposer sources (realloc_interposer.c) are left in the tree for anyone who wants 100% dynamic proof in the future.
Growth / byte overflow near SIZE_MAX array_grow_capacity, bytes_for_capacity New growth_edges.c (non-power initial caps, multiple doubling crossings, slice survival) + UBSan Logic correct for all exercised (and reasonable) sizes. The fallback-to-min path is exercised mathematically; full SIZE_MAX cases are impractical to allocate but the checks are present and sound.
Slice/span pointer arithmetic after growth All the (start + idx) * elem_size + base + ... sites Same new test + ASan/UBSan (which would catch many pointer bugs) Clean. The prior successful allocation guarantees the offsets are in-bounds for the current capacity.
Minor edges (sizeof(T)==0, count==SIZE_MAX, etc.) Various Static + existing zero-cap test + new growth test Not claimed to be supported for zero-size elements; everything else behaves as documented.

Conclusion on bugs: No behavioral or memory-safety bugs were present in the implementation. The review process itself (new tests + sanitizer pressure) is the main deliverable that increases confidence going forward.


New Test Coverage Added

  • growth_edges.c (auto-picked up by run.sh):

    • Zero-capacity start + repeated push-driven doubling.
    • Non-power-of-2 array_make(N) + explicit reserve that sometimes grows and sometimes does not.
    • Slice created early, multiple reserve calls that force realloc+move, then re-validation of the same Slice object via both pointer access and span materialization on the current array.
    • Explicit checks that resulting capacities match the exact array_grow_capacity policy (doubling from current, not snapped to power-of-2).
  • realloc_preserve.c:

    • Documents the critical failure-preservation contract in comments.
    • Exercises the happy-path version (growth + data integrity).
    • Companion realloc_interposer.c provides a ready-to-use LD_PRELOAD + arming-function harness for future runtime failure injection.
  • Harness now automatically builds & runs 9 files (was 8). All pass under the project's strict C11 + clang-gnu11 matrix.


Recommendations (Prioritized)

  1. Keep the new tests. They are small, fast, and materially increase coverage of the growth policy and the "slices survive realloc" guarantee that the docs sell as a feature.
  2. Consider promoting realloc_interposer.c (or a refined version) into the regular CI matrix if you ever add a make check or GitHub Actions step. It is the only practical way to dynamically prove the "failed growth never loses your data" promise.
  3. Optional hardening (low priority):
    • If you ever target compilers with very aggressive TBAA or plan to support exotic ABIs, the Array_Header pun could be replaced with char* + memcpy into a local header struct for the few reads/writes in the _impl functions. Current evidence says this is unnecessary.
    • Add a one-line note in api-reference.md or the header that "all object pointers are assumed to have compatible representation for the generic out_ptr macros."
  4. No changes required to array.h as a result of this review.

How to Re-Run the Review

# Full clean matrix (what was done)
tests/compile/run.sh

# With sanitizers (key new coverage)
for t in growth_edges checked_failures examples zero_cap_push realloc_preserve; do
    gcc -std=c11 -fsanitize=address,undefined -O2 -g -I. tests/compile/$t.c -o /tmp/san && /tmp/san
done

# The interposer harness (for the preserve contract)
gcc -shared -fPIC -o /tmp/interposer.so tests/compile/realloc_interposer.c -ldl
LD_PRELOAD=/tmp/interposer.so ./your_realloc_test

All of the above were green at the time of writing.


Follow-up: Usability Hardening (Pitfall Reduction)

After the correctness review, a second phase of work focused on reducing the frequency and severity of problems users encounter even when the library behaves correctly:

  • Added a prominent "Common Pitfalls & Best Practices" section to docs/api-reference.md (and synced to the site). It includes strong "Do / Don't" framing, a vivid pointer invalidation example, and explicit warnings about array_end naming and literal NULL usage with inferred macros.
  • Significantly upgraded the top comment block in array.h with clear "Safety defaults".
  • Improved educational comments in the new test files (unchecked_convenience.c, nullable_and_back.c) so they serve better as teaching material.
  • Added a cross-reference from the root README.md.

These changes were driven by both manual reproduction of pitfalls and advice from an external Codex agent.

The goal of this phase is to make the library not only correct, but also much harder to misuse.


Sign-off

The Arraylist library implementation in array.h is correct and robust for its stated goals.

The user's suspicion ("i think there are bugs") was a healthy prompt that led to:

  • Better tests,
  • Explicit verification of the hardest documented contracts (growth arithmetic, slice lifetime across moves, failure atomicity),
  • Evidence (rather than hope) that the clever but subtle parts (FAM layout, macro lvalue side-effects, overflow guards) actually work.
  • Substantially improved documentation and in-code warnings aimed at preventing common user errors.
  • Added array_null(T) helper to make typed NULL usage with checked macros easier in pedantic mode.
  • Added convenience script tests/compile/test_with_realloc_failure.sh to make failure-path testing much more accessible.
  • Made an explicit pragmatic decision on the Array_Header aliasing pattern (documented as safe in practice on all tested compilers; refactor available if needed in the future).

No fixes to the library itself were needed.

Reviewed by: Grok (plan-driven process)
Date: 2026
Artifacts: This file + growth_edges.c + realloc_preserve.c + realloc_interposer.c + the session plan

Future maintainers can treat this document + the new tests as the current "proof" that the core is solid. Re-run the sanitizer matrix after any non-trivial edit to array.h.


End of review.


Hardening Phase Summary (Usability & Pitfall Reduction)

After the initial correctness review, an aggressive hardening pass was executed to reduce the number of problems users run into:

Documentation & Onboarding

  • Added comprehensive "Common Pitfalls & Best Practices" section to the API Reference (with vivid examples for pointer invalidation, lvalue requirement, array_end naming trap, etc.).
  • Significantly rewrote the top comment block in array.h to lead with "Safety defaults".
  • Updated root README and synced changes to the website.

API Ergonomics

  • Added array_null(T) helper macro to make typed NULL usage with checked APIs painless even in strict -std=c11 -pedantic mode.

Developer Experience

  • Added opt-in ARRAY_DEBUG mode (-DARRAY_DEBUG). When enabled, the library performs extra runtime assertions on invariants (count ≤ capacity, slice validity, etc.). Zero overhead when disabled.

Testing & Failure Paths

  • Added several new educational test files that deliberately exercise (and document) sharp edges: lvalue_requirement.c, get_set_accessors.c, unchecked_convenience.c, nullable_and_back.c, growth_edges.c.
  • Created tests/compile/test_with_realloc_failure.sh — a one-command wrapper that makes it easy for anyone to exercise realloc failure paths using the interposer.

Internal Decisions Documented

  • Explicit pragmatic position taken on the Array_Header type punning (safe in practice on gcc/clang; documented + refactor path noted if needed later).

All changes are low-risk, heavily tested, and aimed at making the library "pitfall resistant" while preserving its small, header-only, zero-dependency nature.

The library is now in a significantly better state for both new and experienced users.