Skip to content

Commit 4145fe3

Browse files
committed
Tier 8 SECOND-PILOT Phase B: block_map_.bc_blocks → PhxBcBlockArray
Closes the SECOND pilot. Migrates the BasicBlock* → BytecodeInstructionBlock mapping from std::unordered_map to a dense array indexed by BasicBlock::id, exploiting the production-validated invariant that ids are allocation-monotonic from 0 (cfg.h:139-144 `id = next_block_id_++`). Choice of B-γ (dense array) over B-α (parallel hash) per theologian 11:13:21Z: no hash, no collisions, no resize-on-load-factor — three classes of bug B-α can have, B-γ cannot. Pattern is new (id-indexed dense array vs hash) but well-suited to the data shape. Combined with Phase A (push 39, 72bb8e9), block_map_ is ENTIRELY migrated to PhxHirBuilderState. The wrapping BlockMap C++ struct + the block_map_ C++ field are DELETED. Net bridges: 0 (no new bridges; bc_blocks had zero C-side callers per inventory). Per theologian 11:19:42Z: §5 python#11 prevents UNCONTROLLED bridge growth, so a net-0 pilot is neither violation nor satisfaction — Phase B's substantive deliverable is std::unordered_map elimination, which advances the ZERO-C++ terminal goal equivalent to bridge subtraction. PyCodeObject* code is constant per-compile and lives on PhxHirBuilderState.code (set in HIRBuilder ctor), so it is not stored per-array-slot — saves 8 bytes per block. Three invariants asserted (theologian 11:13:21Z): - I1 (JIT_DCHECK at createBlocks return): bc_block_array.count == block_map_phx.count. The two are populated in lockstep on every loop iteration; counts must agree. - I2 (JIT_DCHECK at buildHIRImpl read): tc.block->id < bc_block_array.count. Catches HIR-pass-created blocks accidentally being looked up; should never happen but assert it. - I3 (documented, not asserted): BasicBlock::id is read-only post- allocation. No HIR pass renumbers ids; if any future pass does, B-γ is wrong. RPO and other passes traverse, never renumber. Runtime sentinel would defeat the dense-array O(1) win. Lazy-grow uses single-realloc max(old_cap*2, block_id+1) per theologian 11:19:42Z verification (block_id=1000 with old_cap=16 is 1 alloc, not 7 doublings). CALLER REWRITES (2 functional sites): - builder.cpp createBlocks: writes both state_.block_map_phx (Phase A) and state_.bc_block_array_phx (Phase B) in lockstep. Return type void (was BlockMap). - builder.cpp buildHIRImpl: read site reconstructs BytecodeInstructionBlock value from {code_, BCIndex{start}, BCIndex{end}} using array entry + state_.code (constant per-compile). Subsequent .begin/.end/iteration works on the local value identically to the old map-reference. UNIT TESTS: +6 PhxBcBlockArray tests (init/insert+at/lazy-grow/n300/ clear/overwrite-same-id) appended to test_phx_block_map.c. Existing 8 PhxBlockMap tests unchanged. GATE COVERAGE: the existing block_map_resize_chain wiring fixture (Phase A push 39) exercises createBlocks which now populates BOTH structures in lockstep, so PhxBcBlockArray growth is implicitly covered. Pythia python#119 (a) n=294 coverage extension is a separate followup commit (theologian 11:20:24Z + supervisor 11:20:43Z DEFERRED — sequence-isolated, not bundled). Auth chain: theologian B-γ pick 11:13:21Z + patch-shape APPROVE 11:19:42Z (2 verifications applied at 11:20:15Z); supervisor 11:13:46Z + 11:18:54Z + 11:20:43Z; shepard sequence-isolation 11:11:59Z; librarian §5 python#11 non-violation framing 11:15:58Z.
1 parent 72bb8e9 commit 4145fe3

5 files changed

Lines changed: 253 additions & 39 deletions

File tree

Python/jit/hir/builder.cpp

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,16 +1160,17 @@ static bool should_snapshot(
11601160
}
11611161

11621162
// Compute basic block boundaries and allocate corresponding HIR blocks
1163-
HIRBuilder::BlockMap HIRBuilder::createBlocks(
1163+
void HIRBuilder::createBlocks(
11641164
Function& irfunc,
11651165
const BytecodeInstructionBlock& bc_block) {
1166-
BlockMap block_map;
1167-
/* Tier 8 SECOND-PILOT Phase A: blocks lookup table now lives in
1168-
* state_.block_map_phx. Clear before populating so successive
1169-
* createBlocks calls (e.g. inlined callees via inlineHIR) start with
1170-
* an empty hash, matching the prior re-assignment-of-empty-map
1171-
* semantics of the deleted std::unordered_map field. */
1166+
/* Tier 8 SECOND-PILOT Phase A + B: BlockMap struct deleted. Both
1167+
* sub-fields now live in state_:
1168+
* .blocks → state_.block_map_phx (Phase A: PhxBlockMap)
1169+
* .bc_blocks → state_.bc_block_array_phx (Phase B: dense id-array)
1170+
* Clear both before populating so successive createBlocks calls
1171+
* (e.g. inlined callees via inlineHIR) start empty. */
11721172
phx_block_map_clear(&state_.block_map_phx);
1173+
phx_bc_block_array_clear(&state_.bc_block_array_phx);
11731174

11741175
// Mark the beginning of each basic block in the bytecode
11751176
std::set<BCIndex> block_starts = {BCIndex{0}};
@@ -1229,18 +1230,28 @@ HIRBuilder::BlockMap HIRBuilder::createBlocks(
12291230
end_idx = BCIndex{bc_block.size()};
12301231
}
12311232
auto block = irfunc.cfg.AllocateBlock();
1232-
/* Tier 8 SECOND-PILOT Phase A: blocks lookup migrated from
1233-
* std::unordered_map<BCOffset,BasicBlock*> to PhxBlockMap (custom
1234-
* open-addressed hash) in PhxHirBuilderState.block_map_phx. */
1233+
/* Phase A: BCOffset → BasicBlock* via PhxBlockMap. */
12351234
phx_block_map_insert(
12361235
&state_.block_map_phx, BCOffset{start_idx}.value(), block);
1237-
block_map.bc_blocks.emplace(
1238-
std::piecewise_construct,
1239-
std::forward_as_tuple(block),
1240-
std::forward_as_tuple(bc_block.code(), start_idx, end_idx));
1241-
}
1242-
1243-
return block_map;
1236+
/* Phase B: BasicBlock* → {start, end} via dense id-array. PyCodeObject*
1237+
* code is constant per-compile and lives on state_.code (set in ctor),
1238+
* so it is not stored per-entry. */
1239+
phx_bc_block_array_insert(
1240+
&state_.bc_block_array_phx,
1241+
block->id,
1242+
start_idx.value(),
1243+
end_idx.value());
1244+
}
1245+
/* Phase B I1: at createBlocks return, the bc_block_array and
1246+
* block_map_phx must have matching populated counts (every loop
1247+
* iteration above inserts into BOTH in lockstep). bc_block_array.count
1248+
* is high-water-mark id+1; with allocation-monotonic ids and clear-
1249+
* at-top, it equals the number of inserts this call. */
1250+
JIT_DCHECK(
1251+
state_.bc_block_array_phx.count == state_.block_map_phx.count,
1252+
"Phase B I1: bc_block_array.count ({}) != block_map_phx.count ({})",
1253+
state_.bc_block_array_phx.count,
1254+
state_.block_map_phx.count);
12441255
}
12451256

12461257

@@ -1539,7 +1550,7 @@ BasicBlock* HIRBuilder::buildHIRImpl(
15391550
temps_ = TempAllocator(&irfunc->env);
15401551

15411552
BytecodeInstructionBlock bc_instrs{code_};
1542-
block_map_ = createBlocks(*irfunc, bc_instrs);
1553+
createBlocks(*irfunc, bc_instrs);
15431554
if (frame_state != nullptr) {
15441555
// Suppress exception table for inlined callees to prevent B2
15451556
// (emitBinaryOp -> findExceptionHandler -> emitInlineExceptionMatch)
@@ -1709,8 +1720,20 @@ void HIRBuilder::translate(
17091720
}
17101721
processed.emplace(tc.block);
17111722

1712-
// Translate remaining instructions into HIR
1713-
auto& bc_block = map_get(block_map_.bc_blocks, tc.block);
1723+
// Translate remaining instructions into HIR.
1724+
// Tier 8 SECOND-PILOT Phase B: bc_blocks is now state_.bc_block_array_phx
1725+
// (dense array indexed by BasicBlock::id). I2 invariant: tc.block came
1726+
// from the cfg traversal queue and was inserted by createBlocks; its id
1727+
// must be < high-water-mark.
1728+
JIT_DCHECK(
1729+
(size_t)tc.block->id < state_.bc_block_array_phx.count,
1730+
"Phase B I2: bc_block_array lookup id {} beyond high-water {}",
1731+
tc.block->id,
1732+
state_.bc_block_array_phx.count);
1733+
PhxBcBlockEntry _bc_e =
1734+
phx_bc_block_array_at(&state_.bc_block_array_phx, tc.block->id);
1735+
BytecodeInstructionBlock bc_block{
1736+
code_, BCIndex{_bc_e.start}, BCIndex{_bc_e.end}};
17141737

17151738
// Safety: skip unreachable END_FOR blocks. With _PyOpcode_Deopt in
17161739
// opcode(), getJumpTarget() correctly skips past END_FOR after

Python/jit/hir/builder.h

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,14 @@ class HIRBuilder {
210210
// (push/size/entry) DELETED. findExceptionHandler + parseExceptionTable
211211
// C++ shims rewired internally to PhxExceptionTable; Phase B will
212212
// delete those shims.
213-
// Tier 8 SECOND-PILOT Phase A (theologian 10:25:08Z + supervisor
214-
// 10:25:19Z + 10:29:05Z): block_map_.blocks migrated from
215-
// std::unordered_map<BCOffset,BasicBlock*> to PhxBlockMap (custom
216-
// open-addressed hash) in PhxHirBuilderState.block_map_phx. The
217-
// _cpp lookup bridge + the public hir_builder_get_block_at_off
218-
// C-API DELETED; C-side callers use phx_hir_builder_state +
219-
// phx_block_map_lookup directly. block_map_.bc_blocks (heavy
220-
// BytecodeInstructionBlock value) STAYS on the C++ side this phase.
213+
// Tier 8 SECOND-PILOT Phases A + B: block_map_ ENTIRELY migrated.
214+
// Phase A: .blocks → PhxBlockMap (custom open-addressed hash); the
215+
// _cpp lookup bridge + hir_builder_get_block_at_off C-API DELETED;
216+
// C-side callers use phx_hir_builder_state + phx_block_map_lookup
217+
// directly. Phase B (theologian 11:13:21Z + supervisor 11:13:46Z):
218+
// .bc_blocks → PhxBcBlockArray (dense array indexed by
219+
// BasicBlock::id, exploiting allocation-monotonic id invariant);
220+
// BlockMap struct + block_map_ field DELETED.
221221
friend PhxHirBuilderState *::phx_hir_builder_state(void*);
222222
public:
223223
const Preloader& preloader() const { return preloader_; }
@@ -577,14 +577,11 @@ class HIRBuilder {
577577
const InvokeTarget& target,
578578
TranslationContext& tc,
579579
long nargs);
580-
/* Tier 8 SECOND-PILOT Phase A: BlockMap.blocks migrated to
581-
* PhxBlockMap in PhxHirBuilderState.block_map_phx (custom hash);
582-
* BlockMap retained holding only bc_blocks (heavy
583-
* BytecodeInstructionBlock value-type, deferred to later phase). */
584-
struct BlockMap {
585-
std::unordered_map<BasicBlock*, BytecodeInstructionBlock> bc_blocks;
586-
};
587-
BlockMap createBlocks(
580+
/* Tier 8 SECOND-PILOT Phase A + B: BlockMap struct DELETED. Both
581+
* sub-fields (.blocks, .bc_blocks) migrated to PhxHirBuilderState
582+
* (block_map_phx + bc_block_array_phx). createBlocks now writes
583+
* directly into state_; return type void. */
584+
void createBlocks(
588585
Function& irfunc,
589586
const BytecodeInstructionBlock& bc_block);
590587
BasicBlock* getBlockAtOff(BCOffset off);
@@ -619,7 +616,6 @@ class HIRBuilder {
619616
void advancePastYieldInstr(TranslationContext& tc);
620617

621618
PyCodeObject* code_;
622-
BlockMap block_map_;
623619

624620
// Tier 8 pilot Phase A + Phase B: ExceptionTableEntry struct +
625621
// std::vector<...> exception_table_ field migrated to

Python/jit/hir/builder_state_c.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ void hir_builder_state_init(
3030
state->kwnames = NULL;
3131
phx_exception_table_init(&state->exception_table_phx);
3232
phx_block_map_init(&state->block_map_phx);
33+
phx_bc_block_array_init(&state->bc_block_array_phx);
3334
}
3435

3536
void hir_builder_state_destroy(PhxHirBuilderState *state) {
3637
phx_exception_table_destroy(&state->exception_table_phx);
3738
phx_block_map_destroy(&state->block_map_phx);
39+
phx_bc_block_array_destroy(&state->bc_block_array_phx);
3840
}
3941

4042
void hir_builder_state_parse_exception_table_c(

Python/jit/hir/builder_state_c.h

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
* Class A members (5 immutable + nullable opaque pointers) extracted
99
* from HIRBuilder. Class B members migrated per-batch:
1010
* exception_table_ → PhxExceptionTable (Tier 8 pilot 1 Phase A/B)
11-
* block_map_.blocks → PhxBlockMap (Tier 8 pilot 2 Phase A);
12-
* block_map_.bc_blocks stays C++-side
11+
* block_map_.blocks → PhxBlockMap (Tier 8 pilot 2 Phase A)
12+
* block_map_.bc_blocks → PhxBcBlockArray (Tier 8 pilot 2 Phase B)
1313
* temps_, static_method_stack_ remain C++-side via _cpp bridges
1414
* (Phase 3 Batch 5/6 closure).
1515
* pending_b2_blocks_ dead-state-deleted Phase 3 Batch 3.
@@ -26,6 +26,7 @@
2626
#include <stddef.h>
2727
#include <stdint.h>
2828
#include <stdlib.h>
29+
#include <string.h>
2930

3031
#ifdef __cplusplus
3132
extern "C" {
@@ -222,6 +223,83 @@ static inline void *phx_block_map_lookup(const PhxBlockMap *m, int key) {
222223
return NULL;
223224
}
224225

226+
/* PhxBcBlockEntry — POD mirror of BytecodeInstructionBlock {start, end}
227+
* fields, sufficient to reconstruct one (the third constructor arg
228+
* `code` is constant per-compile and lives on PhxHirBuilderState.code).
229+
* Tier 8 SECOND-PILOT Phase B (theologian 11:13:21Z + supervisor
230+
* 11:06:31Z): replaces the (now-deleted) std::unordered_map<BasicBlock*,
231+
* BytecodeInstructionBlock> bc_blocks field via a dense array indexed
232+
* by BasicBlock::id, exploiting the production-validated invariant that
233+
* BasicBlock ids are allocation-monotonic from 0 (cfg.h:139-144). */
234+
typedef struct PhxBcBlockEntry {
235+
int start; /* BCIndex.value() */
236+
int end; /* BCIndex.value() */
237+
} PhxBcBlockEntry;
238+
239+
typedef struct PhxBcBlockArray {
240+
PhxBcBlockEntry *data;
241+
size_t count; /* high-water-mark id+1 ever inserted (== max id +1) */
242+
size_t capacity; /* allocated slots */
243+
} PhxBcBlockArray;
244+
245+
#define PHX_BC_BLOCK_ARRAY_INITIAL_CAP 16u
246+
247+
static inline void phx_bc_block_array_init(PhxBcBlockArray *a) {
248+
a->data = NULL;
249+
a->count = 0;
250+
a->capacity = 0;
251+
}
252+
253+
static inline void phx_bc_block_array_destroy(PhxBcBlockArray *a) {
254+
if (a->data) {
255+
free(a->data);
256+
a->data = NULL;
257+
}
258+
a->count = 0;
259+
a->capacity = 0;
260+
}
261+
262+
static inline void phx_bc_block_array_clear(PhxBcBlockArray *a) {
263+
a->count = 0;
264+
/* Retain capacity for cheap re-fill on inlined-callee re-createBlocks. */
265+
}
266+
267+
/* Insert at array[block_id] = {start, end}. Lazily grows capacity to
268+
* cover block_id in a SINGLE realloc (theologian 11:19:42Z verification:
269+
* use max(old*2, needed) instead of a doubling loop, so block_id=1000
270+
* with old_cap=16 is one realloc not seven). Intermediate slots between
271+
* old count and block_id are zero-filled (start=0, end=0 sentinel —
272+
* never read because callers only look up block ids actually allocated
273+
* by createBlocks; I2 invariant). */
274+
static inline void phx_bc_block_array_insert(
275+
PhxBcBlockArray *a, int block_id, int start, int end) {
276+
size_t needed = (size_t)block_id + 1u;
277+
if (needed > a->capacity) {
278+
size_t doubled = a->capacity ? a->capacity * 2u : PHX_BC_BLOCK_ARRAY_INITIAL_CAP;
279+
size_t new_cap = doubled > needed ? doubled : needed;
280+
size_t old_cap = a->capacity;
281+
PhxBcBlockEntry *new_data = (PhxBcBlockEntry*)realloc(
282+
a->data, new_cap * sizeof(PhxBcBlockEntry));
283+
memset(new_data + old_cap, 0,
284+
(new_cap - old_cap) * sizeof(PhxBcBlockEntry));
285+
a->data = new_data;
286+
a->capacity = new_cap;
287+
}
288+
a->data[block_id].start = start;
289+
a->data[block_id].end = end;
290+
if (needed > a->count) {
291+
a->count = needed;
292+
}
293+
}
294+
295+
/* Read array[block_id]. Caller must guarantee block_id < a->count
296+
* (invariant I2: never look up an id beyond createBlocks high-water).
297+
* Returns by value; entry is just 8 bytes. */
298+
static inline PhxBcBlockEntry phx_bc_block_array_at(
299+
const PhxBcBlockArray *a, int block_id) {
300+
return a->data[block_id];
301+
}
302+
225303
/* PhxHirBuilderState — opaque holder for HIRBuilder Class A state +
226304
* Tier 8-migrated Class B containers (currently exception_table_phx
227305
* post pilot 1, block_map_phx post pilot 2; remaining 2 Class B
@@ -235,6 +313,7 @@ typedef struct PhxHirBuilderState {
235313
void *kwnames; /* Register* (mutable, nullable) */
236314
PhxExceptionTable exception_table_phx; /* Tier 8 pilot 1 (Phase A) */
237315
PhxBlockMap block_map_phx; /* Tier 8 pilot 2 (Phase A) */
316+
PhxBcBlockArray bc_block_array_phx; /* Tier 8 pilot 2 (Phase B) */
238317
} PhxHirBuilderState;
239318

240319
/* Initialize state_ Class A fields from HIRBuilder ctor args. Mutable

Python/jit/hir/test_phx_block_map.c

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,111 @@ static void test_08_clear(void) {
198198
PASS();
199199
}
200200

201+
/* ================ PhxBcBlockArray (Tier 8 SECOND-PILOT Phase B) ================ */
202+
203+
static void test_b01_init_destroy(void) {
204+
PhxBcBlockArray a;
205+
phx_bc_block_array_init(&a);
206+
ASSERT(a.data == NULL, "init: data NULL");
207+
ASSERT(a.count == 0, "init: count 0");
208+
ASSERT(a.capacity == 0, "init: capacity 0");
209+
phx_bc_block_array_destroy(&a);
210+
PASS();
211+
}
212+
213+
static void test_b02_insert_at(void) {
214+
PhxBcBlockArray a;
215+
phx_bc_block_array_init(&a);
216+
phx_bc_block_array_insert(&a, 0, 100, 200);
217+
phx_bc_block_array_insert(&a, 1, 300, 400);
218+
phx_bc_block_array_insert(&a, 2, 500, 600);
219+
ASSERT(a.count == 3, "count 3 after 3 inserts");
220+
ASSERT(a.capacity == PHX_BC_BLOCK_ARRAY_INITIAL_CAP, "no grow yet");
221+
PhxBcBlockEntry e0 = phx_bc_block_array_at(&a, 0);
222+
PhxBcBlockEntry e1 = phx_bc_block_array_at(&a, 1);
223+
PhxBcBlockEntry e2 = phx_bc_block_array_at(&a, 2);
224+
ASSERT(e0.start == 100 && e0.end == 200, "entry 0");
225+
ASSERT(e1.start == 300 && e1.end == 400, "entry 1");
226+
ASSERT(e2.start == 500 && e2.end == 600, "entry 2");
227+
phx_bc_block_array_destroy(&a);
228+
PASS();
229+
}
230+
231+
static void test_b03_grow_lazy(void) {
232+
PhxBcBlockArray a;
233+
phx_bc_block_array_init(&a);
234+
/* Insert at id=20 with initial cap=16 — must grow to 32. */
235+
phx_bc_block_array_insert(&a, 20, 1000, 2000);
236+
ASSERT(a.capacity >= 21, "cap grew to cover id=20");
237+
ASSERT(a.count == 21, "count is high-water id+1");
238+
PhxBcBlockEntry e = phx_bc_block_array_at(&a, 20);
239+
ASSERT(e.start == 1000 && e.end == 2000, "entry 20 lookup");
240+
/* Grow further: insert at id=100 should bring cap >= 128. */
241+
phx_bc_block_array_insert(&a, 100, 7, 8);
242+
ASSERT(a.capacity >= 101, "cap grew to cover id=100");
243+
ASSERT(a.count == 101, "count tracks new high-water");
244+
PhxBcBlockEntry e100 = phx_bc_block_array_at(&a, 100);
245+
ASSERT(e100.start == 7 && e100.end == 8, "entry 100 lookup");
246+
/* Earlier entry survived realloc. */
247+
PhxBcBlockEntry e20 = phx_bc_block_array_at(&a, 20);
248+
ASSERT(e20.start == 1000 && e20.end == 2000, "entry 20 survived realloc");
249+
phx_bc_block_array_destroy(&a);
250+
PASS();
251+
}
252+
253+
static void test_b04_n300(void) {
254+
PhxBcBlockArray a;
255+
phx_bc_block_array_init(&a);
256+
/* 300 sequential inserts, mirrors hot-path createBlocks. */
257+
for (int i = 0; i < 300; i++) {
258+
phx_bc_block_array_insert(&a, i, i * 10, i * 10 + 5);
259+
}
260+
ASSERT(a.count == 300, "count 300");
261+
ASSERT(a.capacity >= 300, "cap >= 300");
262+
for (int i = 0; i < 300; i++) {
263+
PhxBcBlockEntry e = phx_bc_block_array_at(&a, i);
264+
ASSERT(e.start == i * 10 && e.end == i * 10 + 5, "n300 entry");
265+
}
266+
phx_bc_block_array_destroy(&a);
267+
PASS();
268+
}
269+
270+
static void test_b05_clear(void) {
271+
PhxBcBlockArray a;
272+
phx_bc_block_array_init(&a);
273+
for (int i = 0; i < 10; i++) {
274+
phx_bc_block_array_insert(&a, i, i, i + 1);
275+
}
276+
size_t cap_before = a.capacity;
277+
phx_bc_block_array_clear(&a);
278+
ASSERT(a.count == 0, "count reset");
279+
ASSERT(a.capacity == cap_before, "capacity preserved");
280+
/* Re-fill works (mirrors successive createBlocks for inlined callees). */
281+
phx_bc_block_array_insert(&a, 0, 99, 100);
282+
PhxBcBlockEntry e = phx_bc_block_array_at(&a, 0);
283+
ASSERT(e.start == 99 && e.end == 100, "post-clear refill at id 0");
284+
ASSERT(a.count == 1, "count restarts at 1");
285+
phx_bc_block_array_destroy(&a);
286+
PASS();
287+
}
288+
289+
static void test_b06_overwrite_same_id(void) {
290+
/* Mirrors a hypothetical re-insert at same id — last wins.
291+
* Matches BlockMap.bc_blocks.emplace which would no-op on dup-key,
292+
* BUT the array uses last-insert-wins. createBlocks never inserts
293+
* the same id twice in a single call (allocation-monotonic), so
294+
* semantic difference is unobservable in production. */
295+
PhxBcBlockArray a;
296+
phx_bc_block_array_init(&a);
297+
phx_bc_block_array_insert(&a, 5, 1, 2);
298+
phx_bc_block_array_insert(&a, 5, 99, 100);
299+
PhxBcBlockEntry e = phx_bc_block_array_at(&a, 5);
300+
ASSERT(e.start == 99 && e.end == 100, "overwrite latest wins");
301+
ASSERT(a.count == 6, "count is high-water id+1, unchanged");
302+
phx_bc_block_array_destroy(&a);
303+
PASS();
304+
}
305+
201306
int main(void) {
202307
printf("PhxBlockMap unit tests\n");
203308
printf("======================\n");
@@ -209,6 +314,15 @@ int main(void) {
209314
test_06_overwrite();
210315
test_07_lookup_miss();
211316
test_08_clear();
317+
printf("\n");
318+
printf("PhxBcBlockArray unit tests\n");
319+
printf("==========================\n");
320+
test_b01_init_destroy();
321+
test_b02_insert_at();
322+
test_b03_grow_lazy();
323+
test_b04_n300();
324+
test_b05_clear();
325+
test_b06_overwrite_same_id();
212326
printf("======================\n");
213327
printf("Result: %d PASS / %d FAIL\n", g_pass, g_fail);
214328
return g_fail == 0 ? 0 : 1;

0 commit comments

Comments
 (0)