@@ -48,8 +48,20 @@ pub fn BTreeMap(
4848 comptime compare : fn (lhs : K , rhs : K ) std.math.Order ,
4949 comptime BRANCHING_FACTOR : u16 ,
5050) type {
51- std .debug .assert (BRANCHING_FACTOR >= 3 );
52- const MIN_KEYS = (BRANCHING_FACTOR - 1 ) / 2 ;
51+ comptime {
52+ if (BRANCHING_FACTOR < 4 ) {
53+ @compileError ("BTreeMap: BRANCHING_FACTOR must be at least 4" );
54+ }
55+ }
56+ // Minimum keys per non-root node.
57+ //
58+ // We pick this so that `2 * MIN_KEYS + 1` (the size of a merge of two
59+ // MIN_KEYS siblings plus the pulled-down separator) always fits in the
60+ // `BRANCHING_FACTOR - 1`-element key array. For even B this simplifies
61+ // to `(B - 1) / 2` — identical to the common textbook formula. For odd
62+ // B it relaxes MIN_KEYS by one compared to the textbook, which avoids
63+ // an over-full node after merge at the cost of slightly looser packing.
64+ const MIN_KEYS = (BRANCHING_FACTOR - 2 ) / 2 ;
5365
5466 return struct {
5567 const Self = @This ();
@@ -529,6 +541,11 @@ pub fn BTreeMap(
529541
530542 fn init (allocator : std.mem.Allocator , root : ? * Node ) ! Iterator {
531543 var stack : std .ArrayList (StackFrame ) = .empty ;
544+ // If any append below fails after the first, the local
545+ // `stack` goes out of scope without being moved into the
546+ // returned Iterator; its heap buffer would leak without
547+ // this errdefer.
548+ errdefer stack .deinit (allocator );
532549
533550 if (root ) | r | {
534551 try stack .append (allocator , StackFrame { .node = r , .index = 0 });
@@ -783,3 +800,62 @@ test "BTreeMap: negative keys" {
783800 try std .testing .expectEqual (@as (i32 , 10 ), map .get (-10 ).?.* );
784801 try std .testing .expectEqual (@as (i32 , -5 ), map .get (5 ).?.* );
785802}
803+
804+ test "regression: BTreeMap deletion works with odd BRANCHING_FACTOR" {
805+ // Bug B1: the old `MIN_KEYS = (B-1)/2` formula combined with the split
806+ // layout left odd-B right siblings underfull; a subsequent deletion that
807+ // merged two MIN_KEYS siblings plus a separator overflowed the
808+ // `[B-1]K` keys array, panicking at merge time. This test exercises
809+ // the exact path that used to crash for B=5.
810+ const allocator = std .testing .allocator ;
811+ var map = BTreeMap (i32 , i32 , i32Compare , 5 ).init (allocator );
812+ defer map .deinit ();
813+
814+ // Fill, split, then fill again so both halves are at MIN_KEYS.
815+ try map .put (0 , 0 );
816+ try map .put (1 , 1 );
817+ try map .put (2 , 2 );
818+ try map .put (3 , 3 );
819+ try map .put (4 , 4 );
820+ try std .testing .expectEqual (@as (usize , 5 ), map .count ());
821+
822+ // Delete from the right side: this forces the merge path.
823+ try std .testing .expectEqual (@as (i32 , 4 ), map .remove (4 ).? );
824+ try std .testing .expectEqual (@as (usize , 4 ), map .count ());
825+
826+ // Verify the remaining tree is intact and ordered.
827+ try std .testing .expectEqual (@as (i32 , 0 ), map .get (0 ).?.* );
828+ try std .testing .expectEqual (@as (i32 , 1 ), map .get (1 ).?.* );
829+ try std .testing .expectEqual (@as (i32 , 2 ), map .get (2 ).?.* );
830+ try std .testing .expectEqual (@as (i32 , 3 ), map .get (3 ).?.* );
831+ try std .testing .expect (map .get (4 ) == null );
832+ }
833+
834+ test "regression: BTreeMap sequential delete with odd B stays valid" {
835+ // Larger stress: insert 200 items with B=7 (odd, previously broken),
836+ // delete half in random-ish order, then confirm the remainder is still
837+ // accessible and ordered.
838+ const allocator = std .testing .allocator ;
839+ var map = BTreeMap (i32 , i32 , i32Compare , 7 ).init (allocator );
840+ defer map .deinit ();
841+
842+ var i : i32 = 0 ;
843+ while (i < 200 ) : (i += 1 ) {
844+ try map .put (i , i * 2 );
845+ }
846+ try std .testing .expectEqual (@as (usize , 200 ), map .count ());
847+
848+ // Delete every other item in ascending order.
849+ i = 0 ;
850+ while (i < 200 ) : (i += 2 ) {
851+ const v = map .remove (i );
852+ try std .testing .expectEqual (@as (i32 , i * 2 ), v .? );
853+ }
854+ try std .testing .expectEqual (@as (usize , 100 ), map .count ());
855+
856+ // Remaining items must still be reachable with correct values.
857+ i = 1 ;
858+ while (i < 200 ) : (i += 2 ) {
859+ try std .testing .expectEqual (@as (i32 , i * 2 ), map .get (i ).?.* );
860+ }
861+ }
0 commit comments