Skip to content

Review fixes and API pass: correctness fixes, validity-returning navigation, set iterators, Span queries, CommutativeMonoid - #5

Merged
ajwerner merged 7 commits into
mainfrom
api
Sep 29, 2026
Merged

ajwerner merged 7 commits into
mainfrom
api

Conversation

@ajwerner

@ajwerner ajwerner commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

One PR for all three review rounds on v0.2.0 and the API design pass. Breaking on top of v0.2.0; the changelog lists every renamed or re-typed symbol. Tag after merge: v0.3.0.

Correctness fixes (each with a test; those runnable against v0.2.0 fail there)

  • Cursor.Upsert with a distinct but equal key replaced only the value and misreported PrevKey.
  • The interval bound started from the zero endpoint: wrong for negative endpoints, a nil dereference for pointer endpoints.
  • Verify copied the atomic reference count non-atomically and wrote augmentations of shared nodes.
  • Shape-dependent augmentations (node counts, heights) never heard about splits, merges and rebalances; they now implement Restructurer to be told (the reviewer's degree-2 example: root count 3 instead of 4). Entry-only augmentations pay nothing.
  • Nodes start from the monoid's Identity, fresh and recycled; a product monoid (identity 1) aggregated to 0 before.
  • Reference counts are 64-bit.
  • Empty and reversed intervals are points under the default HasEnd; a custom HasEnd that claims an end for one is rejected at insertion. The default tie-break treats every representation of a point at a key as the same item.
  • Verify uses the Updater's Equaler (NaN-safe aggregates) and, for interval trees, checks that intervals are ordered by start.

API pass

  1. Every positioning method returns whether the iterator is now at an entry; SeekExact answers "does the key exist".
  2. Comparator equality is identity: Set.Get(probe), Map.Lookup(k), item-returning Set.Delete, cursor Upsert/Rekey returning displaced entries; the comparison contract and NewOrdered's NaN limit are documented.
  3. SetIterator/SetCursor for btree, orderstat and interval (plus SetOverlapIterator) with Item and no Value, SetValue or phantom arguments. Cursor.Delete documents the deletion loop.
  4. Interval queries are Spans (HalfOpen, Point) separate from the stored type; Overlaps(span) returns an OverlapIterator whose Next is always the next overlap and whose Seek reuses it; the plain Iterator has no overlap mode. Bounds.TieBreak orders equal starts; CompareIntervals remains as an optional single-call comparator that must order by start (checked by Verify).
  5. MonoidMap has no exported embedded field; Node.Aug/ChildAug return values and SetAug is for Updaters; LowLevelIterator.Config returns a copy and Config.Compare has a value receiver.
  6. CommutativeMonoid/CommutativeGroup with Identity; SeekPrefix(inclusivePrefix) with a once-flipping predicate replaces SeekWhere.
  7. New panics on a nil comparison function; the zero Map is documented unusable; one ownership contract in the aug docs; Map.Iterator's doc says to reposition after a mutation.

Not done: type-checked free-list options. A btree.FreeList[int, int] can still be passed to a btree.New[int, string] and only fails at construction, because functional options cannot be typed and inferable in Go without type arguments at every call site. A typed config struct would fix it at the cost of every constructor call; left as an accepted trade-off.

Performance

In-repo benchmarks against v0.2.0 in the same session (benchstat, 4 runs): non-interval benchmarks at parity or better; the interval package, which regressed up to 3× in the first cut of the API pass (per-query iterator construction, a recompute on every merge, an extra comparison per bound), is back to parity or ahead (geomean -1.3%) after OverlapIterator.Seek, opt-in Restructurer and HasEnd-only bounds. The google/btree harness comparison was run interleaved (five alternations of v0.2.0 and the branch, compiled binaries, same machine state) because the machine drifted mid-session. The google/btree control rows, identical code on both sides, moved by up to ±15% between columns, which is the noise floor for that run; within it the branch is at parity (geomean -5%), with no row outside the control's own swing. Same-session in-repo benchstat (quieter machine, 4 runs) remains the reliable A/B: parity or better throughout.

Test plan

  • go test -race ./... on Go 1.26.8 and 1.27.1
  • Ported CockroachDB interval suite, property tests and fuzzers pass on the new API
  • benchstat against v0.2.0 for the in-repo benchmarks
  • CI

🤖 Generated with Claude Code

ajwerner and others added 3 commits September 29, 2026 07:18
…ext/Prev, Verify ref copy

- Cursor.Upsert's in-place path kept the old key when the new one compared
  equal and reported the new key as PrevKey; it now replaces the key and
  reports the real previous one. Tested with keys compared by id and an
  augmentation that depends on the key.
- The interval augmentation started from the zero endpoint as its bound:
  wrong for negative endpoints and a panic for comparators that cannot
  take the zero value. subtreeBound now has an explicit unset state and
  findUpperBound reports emptiness. Tested with pointer endpoints and
  negative spans in the property test.
- interval.Iterator.Next and Prev end an overlap scan, as the seeks do.
- Verify copies only the fields the Updater reads instead of the whole
  node, whose reference count another goroutine may be changing
  atomically. A race test verifies a snapshot while the original is
  cloned, written and cleared.
- SeekWhere's documented contract now matches its single-descent
  algorithm: the predicate must be exact for spans. The aug package doc
  states that maps must not be copied by value and that keys, values and
  augmentations are shallow-copied.

Each new test fails on v0.2.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ints and TieBreak, config copy, Verify equality

- A split, merge or rebalance recomputes the restructured node and
  reports a change upward, so shape-dependent augmentations (node
  counts, heights) stay correct, including the merge caused by deleting
  an absent key. Tested with a node-counting Updater; on v0.2.0 the
  reviewer's degree-2 example leaves the root at 3 instead of 4.
- Reference counts are int64.
- An interval whose end is not after its start is a point, so leaf
  matching and subtree pruning agree wherever it sits; reversed intervals
  are points too. Tested alone in a leaf, after many splits, and reversed.
- interval.Bounds.TieBreak replaces CompareIntervals and is consulted only
  for equal start keys, since overlap searches require start order.
  Tested with a tie-breaker preferring ends.
- LowLevelIterator.Config returns a copy.
- Verify compares augmentations with the Updater's Equal when it
  implements Equaler; MonoidUpdater forwards the Monoid's. Tested with a
  NaN-propagating maximum.
- interval.Cursor, FreeList and NewFreeList are nameable; the package
  documents its overlap cost honestly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ies, CommutativeMonoid

Iterators and cursors: every positioning method (First, Last, Next,
Prev, the seeks, SeekNth) returns whether the iterator is now at an
entry, so a range loop is "for ok := it.SeekGE(lo); ok && ...; ok =
it.Next()". SeekGE and SeekLT no longer report whether the sought key
exists; SeekExact does. Cursor.Upsert returns the replaced key and value
like Map.Upsert, Rekey returns the entry it displaced, and Delete
documents the deletion loop.

Sets: Get(probe) returns the stored item equal to a probe and Delete
returns the removed item, since comparator equality defines identity.
SetIterator and SetCursor are their own types with Item and no Value,
SetValue or phantom value arguments. The btree package documents the
comparison contract and NewOrdered's NaN limitation.

interval: queries are Spans (HalfOpen, Point) separate from the stored
type; Overlaps(span) returns an OverlapIterator whose Next always means
the next overlap and Iterator has no overlap mode; Bounds.HasEnd defaults
to "every interval has an end, and one whose end is not after its start
is a point" instead of the zero-end convention. Empty and reversed query
spans overlap nothing.

aug: Monoid and Group are CommutativeMonoid and CommutativeGroup with an
Identity method, and the zero value is no longer assumed to be the
identity. SeekWhere(prefix, contribution) is SeekPrefix(inclusivePrefix)
with a predicate that turns true at most once, returning the prefix and
whether an entry was found. Node.Aug and ChildAug return values and
Node.SetAug is for Updaters, so traversal cannot modify augmentations;
LowLevelIterator.Config already returns a copy. MonoidMap is a defined
type over Map with no exported embedded field. New panics on a nil
comparison function, the zero Map is documented as unusable, and the
package states one ownership contract for every collection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ajwerner
ajwerner changed the base branch from review-fixes to main September 29, 2026 11:41
@ajwerner ajwerner changed the title API pass: positioning reports validity, real set iterators, Span queries, CommutativeMonoid Review fixes and API pass: correctness fixes, validity-returning navigation, set iterators, Span queries, CommutativeMonoid Sep 29, 2026
ajwerner and others added 4 commits September 29, 2026 08:30
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…usable overlap iterator

- Shape-dependent augmentations opt into structural events by
  implementing Restructurer; the tree no longer recomputes every node
  after a split, merge or rebalance for augmentations that depend only on
  the entries, which had made the interval bound recompute run on every
  merge (Delete +30-70% in the interval benchmarks).
- Nodes start from the monoid's Identity, fresh and recycled, so a monoid
  whose identity is not the zero value (a product) aggregates correctly.
- interval.Set gets Get, an item-returning Delete, SetIterator, SetCursor
  and SetOverlapIterator with Item and no values. The default TieBreak
  treats every representation of a point at the same key as the same
  item. Bounds.CompareIntervals is back as an optional single-call
  comparator that must order by start, which Verify now checks.
- OverlapIterator.Seek restarts a scan on an existing iterator, so a
  query no longer costs a fresh 300-byte iterator; the ported benchmarks
  use it.
- Config.Compare has a value receiver; the interval updater stores its
  bound without a deferred closure; Next and Prev return their validity
  from the position they already know; Map.Iterator's doc says to
  reposition rather than recreate after a mutation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ext fast path

- The point-or-range decision is HasEnd's alone: the default HasEnd
  normalises (an end not after the start makes a point) and a custom
  HasEnd is trusted, so computing a bound costs no comparison; an
  interval a custom HasEnd claims an end for that does not end after it
  starts panics at insertion or replacement.
- OverlapIterator caches the updater instead of copying the Config and
  type-asserting on every step.
- Iterator.Next returns without settling when the next entry is in the
  same leaf.

Against v0.2.0 the interval and iteration benchmarks are now within noise
or ahead (geomean -1.3%); the rest of the suite was already at parity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…descents

Set.Get had built an iterator and sought; it now uses Map.Lookup, a Get
that also returns the stored key, and Contains uses Get directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ajwerner
ajwerner merged commit e94cb9f into main Sep 29, 2026
4 checks passed
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