Skip to content

Commit b44fe59

Browse files
committed
fix(codec): address owner review on PR-195 — escape allocator + NEWS doc + diagram
P1 — escape allocator collision in batched encoding: Old: cfg.escape_next_idx: Option<u32> was a static field — every Escape leaf in a batch got the same idx, colliding on the escape vector slot at decode time. New: predict_intra now takes a third arg `escape_next: Option<&mut u32>` that the kernel post-increments when Escape fires. Sequential cells in the same batch see fresh, non-colliding idxs. IntraConfig becomes empty (reserved for future RDO knobs). + 1 regression test: escape_allocator_advances_across_batched_calls + escape_when_delta_overflows_i8_and_allocator_present extended to assert the cursor advances. P1 — NESW vs NEWS doc/code mismatch: MergeDir discriminants are North=0, East=1, West=2, South=3 (NEWS), but the doc said "NESW". Fixed in IntraContext docstring + the inline Merge-scan comment. Added explicit slot-to-direction table. + 1 regression test: merge_slot_2_maps_to_west_and_slot_3_to_south P2 — ASCII header diagram in mode.rs: Old diagram put M0/M1 at MSB bits 15-14, but pack_header uses `(mode << 12) | basin`, placing mode at bits 12-13 with bits 14-15 reserved at zero. Redrew the diagram with explicit bit indices and labelled the reserved high bits for future encoder use. Nits: - A2 doc table now says "Merge tail: 1 byte (low 2 bits = MergeDir; high 6 reserved)" instead of the misleading "MergeDir 2-bit". - Renamed merge_picks_first_hit_in_nesw_order → ..._in_news_order to match the corrected ordering. Already-addressed (no-op this commit): - P0 overflow Merge alias → fixed in b39a576 - P2 pack_leaf 6-byte minimum → fixed in b39a576 Gates: cargo test --features codec --lib hpc::codec → 50 passed (+2 new) cargo test --features codec --doc hpc::codec → 15 passed (+1 new) cargo fmt --all -- --check → clean cargo clippy --features codec --lib -- -D warnings → clean
1 parent b39a576 commit b44fe59

2 files changed

Lines changed: 136 additions & 77 deletions

File tree

src/hpc/codec/mode.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,32 @@
1313
//! decoder can route on a single `u16` load:
1414
//!
1515
//! ```text
16-
//! MSB LSB
17-
//! ┌──┬──┬──────────────────────────────┐
18-
//! │M0│M1│ basin_idx (12) │ ← 16-bit header
19-
//! └──┴──┴──────────────────────────────┘
20-
//! └─ basin_idx is the only payload field always present
21-
//! └──┴──── 2-bit mode discriminant (CellMode::as_u8())
22-
//! (top 2 bits)
16+
//! bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
17+
//! ┌──┬──┬──┬──┬──────────────────────┐
18+
//! │ 0│ 0│M1│M0│ basin_idx (12) │ ← 16-bit header
19+
//! └──┴──┴──┴──┴──────────────────────┘
20+
//! │ │ │ └────────────────────── basin_idx (bits 0..=11)
21+
//! │ │ └──┴────────────────────────── 2-bit mode (bits 12..=13)
22+
//! └──┴──────────────────────────────── reserved high bits 14,15
2323
//! ```
2424
//!
25-
//! The remaining 2 bits at the top of the second byte are reserved for
26-
//! the encoder's future `merge_dir` overlap when the mode is `Merge`;
27-
//! a separate `pack_mode_dir` helper keeps `Merge`'s direction in a
28-
//! single byte alongside `Skip`/`Delta`/`Escape`'s mode tag.
25+
//! Bits 14-15 are reserved at zero; the impl is
26+
//! `(mode_bits << 12) | basin_bits`, so mode lives at bits 12-13 and
27+
//! basin at bits 0-11. A future encoder can repurpose bits 14-15
28+
//! (e.g., for a per-leaf `merge_dir` overlap) without disturbing
29+
//! existing decoders that mask bits 14-15 off.
2930
//!
3031
//! # Per-mode tail width
3132
//!
3233
//! | Mode | Header | Tail bytes | Total |
3334
//! |--------|--------|--------------------------|-------|
34-
//! | Skip | 2 | 0 | 2 |
35-
//! | Merge | 2 | 1 (`MergeDir` 2-bit) | 3 |
36-
//! | Delta | 2 | 1 (`u8` perturbation) | 3 |
37-
//! | Escape | 2 | 4 (`u32` escape_idx, LE) | 6 |
35+
//! | Skip | 2 | 0 | 2 |
36+
//! | Merge | 2 | 1 (low 2 bits = `MergeDir`) | 3 |
37+
//! | Delta | 2 | 1 (`u8` perturbation) | 3 |
38+
//! | Escape | 2 | 4 (`u32` escape_idx, LE) | 6 |
39+
//!
40+
//! The Merge tail is a full byte even though only its low 2 bits carry
41+
//! `MergeDir` — high 6 bits are reserved and masked off on read.
3842
//!
3943
//! The compact pack writes header (LE) then the per-mode tail. The
4044
//! `escape_idx` width is the worst case; a future A7 rANS pass can

src/hpc/codec/predict.rs

Lines changed: 117 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,17 @@ use super::mode::BASIN_NONE;
8484
/// to decide between Delta (fits in i8) and Escape (overflows).
8585
/// `i32` width avoids overflow when the caller computes
8686
/// `cell_value - basin_value` for two u8 inputs.
87-
/// - `neighbours`: NESW (in [`MergeDir`] discriminant order) optional
88-
/// neighbour leaves. `None` for boundary cells; the Merge candidate
89-
/// scan skips `None` entries.
87+
/// - `neighbours`: NEWS (in [`MergeDir`] discriminant order:
88+
/// `North=0, East=1, West=2, South=3`) optional neighbour leaves.
89+
/// `None` for boundary cells; the Merge candidate scan skips `None`
90+
/// entries.
91+
///
92+
/// ```text
93+
/// slot 0 → MergeDir::North (discr 0)
94+
/// slot 1 → MergeDir::East (discr 1)
95+
/// slot 2 → MergeDir::West (discr 2)
96+
/// slot 3 → MergeDir::South (discr 3)
97+
/// ```
9098
///
9199
/// ```
92100
/// use ndarray::hpc::codec::{IntraContext, LeafCu};
@@ -105,41 +113,27 @@ pub struct IntraContext<'a> {
105113
/// Signed delta from basin → cell, in the basin's u8 quantisation
106114
/// space.
107115
pub delta_i32: i32,
108-
/// NESW neighbour leaves, indexed by [`MergeDir`] discriminant.
116+
/// NEWS neighbour leaves, indexed by [`MergeDir`] discriminant
117+
/// (`North=0, East=1, West=2, South=3`).
109118
pub neighbours: [Option<&'a LeafCu>; 4],
110119
}
111120

112121
/// Configuration for the intra-prediction decision.
113122
///
114-
/// Today a single field; the field exists so the API can grow
115-
/// (Merge tolerance, RDO knobs in A6) without a signature break.
123+
/// Reserved for future expansion (Merge tolerance, RDO knobs in A6).
124+
/// Empty today; constructed via [`Default`] so additions don't break
125+
/// callers.
116126
///
117127
/// ```
118128
/// use ndarray::hpc::codec::IntraConfig;
119-
/// let default_cfg = IntraConfig::default();
120-
/// assert!(default_cfg.escape_next_idx.is_none());
121-
/// let allocated = IntraConfig { escape_next_idx: Some(42) };
122-
/// assert_eq!(allocated.escape_next_idx, Some(42));
129+
/// let cfg = IntraConfig::default();
130+
/// // No tunables yet — call sites stay future-compatible.
131+
/// let _ = cfg;
123132
/// ```
124-
#[derive(Debug, Clone, Copy)]
133+
#[derive(Debug, Clone, Copy, Default)]
125134
pub struct IntraConfig {
126-
/// Future allocator for the encoder's escape vector — returns the
127-
/// next index to write. `None` disables Escape mode (the encoder
128-
/// will fall back to Delta-with-truncation, which **loses
129-
/// precision** but never panics; callers wanting lossless coding
130-
/// must provide a real allocator).
131-
///
132-
/// Stateless API today: encoder calls `escape_next_idx` once per
133-
/// Escape decision. The caller is responsible for actually
134-
/// appending the u64 cell value into the escape vector at the
135-
/// returned index — this kernel doesn't see the cell value.
136-
pub escape_next_idx: Option<u32>,
137-
}
138-
139-
impl Default for IntraConfig {
140-
fn default() -> Self {
141-
Self { escape_next_idx: None }
142-
}
135+
// Reserved. Future fields land here without breaking the signature.
136+
_reserved: (),
143137
}
144138

145139
// ════════════════════════════════════════════════════════════════════
@@ -152,6 +146,14 @@ impl Default for IntraConfig {
152146
/// See the module docs for the decision tree (Skip → Merge → Delta →
153147
/// Escape) and the rationale (monotone wire cost).
154148
///
149+
/// `escape_next` is a write-cursor into the caller's per-frame escape
150+
/// vector. When the decision falls through to Escape, the kernel reads
151+
/// the cursor, emits a leaf referencing that idx, and post-increments
152+
/// the cursor so subsequent cells in the same batch get fresh,
153+
/// non-colliding idxs. Pass `None` to disable lossless Escape — the
154+
/// kernel then clamps `δ` to i8 range and emits a `Delta` leaf whose
155+
/// reconstruction is **not bit-exact** (caller must accept the loss).
156+
///
155157
/// # Examples
156158
///
157159
/// Skip when the cell is exactly the basin:
@@ -164,7 +166,7 @@ impl Default for IntraConfig {
164166
/// delta_i32: 0,
165167
/// neighbours: [None; 4],
166168
/// };
167-
/// let leaf = predict_intra(&ctx, &IntraConfig::default());
169+
/// let leaf = predict_intra(&ctx, &IntraConfig::default(), None);
168170
/// assert_eq!(leaf.mode, CellMode::Skip);
169171
/// assert_eq!(leaf.basin_idx, 42);
170172
/// ```
@@ -179,11 +181,26 @@ impl Default for IntraConfig {
179181
/// delta_i32: 17,
180182
/// neighbours: [None; 4],
181183
/// };
182-
/// let leaf = predict_intra(&ctx, &IntraConfig::default());
184+
/// let leaf = predict_intra(&ctx, &IntraConfig::default(), None);
183185
/// assert_eq!(leaf.mode, CellMode::Delta);
184186
/// assert_eq!(leaf.delta, Some(17));
185187
/// ```
186-
pub fn predict_intra(ctx: &IntraContext, cfg: &IntraConfig) -> LeafCu {
188+
///
189+
/// Escape with an allocator — repeated calls bump the cursor:
190+
///
191+
/// ```
192+
/// use ndarray::hpc::codec::predict::{predict_intra, IntraContext, IntraConfig};
193+
/// use ndarray::hpc::codec::CellMode;
194+
/// let mut next = 7u32;
195+
/// let ctx = IntraContext { basin_idx: 1, delta_i32: 1000, neighbours: [None; 4] };
196+
/// let a = predict_intra(&ctx, &IntraConfig::default(), Some(&mut next));
197+
/// let b = predict_intra(&ctx, &IntraConfig::default(), Some(&mut next));
198+
/// assert_eq!(a.escape_idx, Some(7));
199+
/// assert_eq!(b.escape_idx, Some(8));
200+
/// assert_eq!(next, 9);
201+
/// assert_eq!(a.mode, CellMode::Escape);
202+
/// ```
203+
pub fn predict_intra(ctx: &IntraContext, _cfg: &IntraConfig, escape_next: Option<&mut u32>) -> LeafCu {
187204
// ── 1. Skip ──────────────────────────────────────────────────────
188205
if ctx.delta_i32 == 0 {
189206
return LeafCu::skip(ctx.basin_idx);
@@ -209,7 +226,8 @@ pub fn predict_intra(ctx: &IntraContext, cfg: &IntraConfig) -> LeafCu {
209226
// wrapping cast; matches the A2 pack format where Delta
210227
// stores a raw u8 byte without a sign bit)
211228
//
212-
// We scan NESW in discriminant order and pick the first match.
229+
// We scan NEWS in discriminant order (N=0, E=1, W=2, S=3) and
230+
// pick the first match.
213231
// Multiple matches all collapse to the same coded leaf, so the
214232
// first-hit policy is order-deterministic without affecting
215233
// bitstream length.
@@ -242,16 +260,23 @@ pub fn predict_intra(ctx: &IntraContext, cfg: &IntraConfig) -> LeafCu {
242260

243261
// ── 4. Escape ────────────────────────────────────────────────────
244262
//
245-
// |δ| doesn't fit in i8. Caller must own the per-frame escape
246-
// vector and provide the next-write index; we return a leaf that
247-
// references it. If the caller didn't provide an allocator, we
248-
// fall back to a saturated Delta (lossy but never panicking) so
249-
// a misconfigured encoder still produces a valid bytestream.
250-
match cfg.escape_next_idx {
251-
Some(idx) => LeafCu::escape(ctx.basin_idx, idx),
263+
// |δ| doesn't fit in i8. The cursor `escape_next` is a write-pointer
264+
// into the caller's per-frame escape vector; we read it, emit a
265+
// leaf referencing that idx, and post-increment so subsequent
266+
// overflow cells in the batch don't collide on the same vector
267+
// slot. If the caller didn't provide an allocator, we fall back to
268+
// a saturated Delta (lossy: reconstruction is NOT bit-exact, but
269+
// never panicking) so a misconfigured encoder still produces a
270+
// valid bytestream. The lossy leaf's `mode` is `CellMode::Delta`
271+
// even though its semantic value overflowed i8 — by contract the
272+
// caller has acknowledged the precision loss.
273+
match escape_next {
274+
Some(next) => {
275+
let idx = *next;
276+
*next = next.wrapping_add(1);
277+
LeafCu::escape(ctx.basin_idx, idx)
278+
}
252279
None => {
253-
// Lossy fallback: clamp to i8 range. Caller is responsible
254-
// for noticing that the reconstruction won't be bit-exact.
255280
let clamped = ctx.delta_i32.clamp(-128, 127) as u8;
256281
LeafCu::delta(ctx.basin_idx, clamped)
257282
}
@@ -301,7 +326,7 @@ mod tests {
301326

302327
#[test]
303328
fn skip_when_delta_is_zero() {
304-
let leaf = predict_intra(&ctx_with_neighbours(100, 0, [None; 4]), &IntraConfig::default());
329+
let leaf = predict_intra(&ctx_with_neighbours(100, 0, [None; 4]), &IntraConfig::default(), None);
305330
assert_eq!(leaf, LeafCu::skip(100));
306331
}
307332

@@ -310,14 +335,14 @@ mod tests {
310335
// δ=0 trumps everything else, even a perfect Merge candidate.
311336
let nb = LeafCu::delta(100, 0);
312337
let neighbours = [Some(&nb), None, None, None];
313-
let leaf = predict_intra(&ctx_with_neighbours(100, 0, neighbours), &IntraConfig::default());
338+
let leaf = predict_intra(&ctx_with_neighbours(100, 0, neighbours), &IntraConfig::default(), None);
314339
assert_eq!(leaf.mode, CellMode::Skip);
315340
}
316341

317342
#[test]
318343
fn delta_in_i8_range() {
319344
for d in [-128i32, -1, 1, 127] {
320-
let leaf = predict_intra(&ctx_with_neighbours(100, d, [None; 4]), &IntraConfig::default());
345+
let leaf = predict_intra(&ctx_with_neighbours(100, d, [None; 4]), &IntraConfig::default(), None);
321346
assert_eq!(leaf.mode, CellMode::Delta);
322347
assert_eq!(leaf.delta, Some(d as u8));
323348
}
@@ -328,7 +353,7 @@ mod tests {
328353
// Northern neighbour: Delta-mode, same basin, same δ as us.
329354
let nb_north = LeafCu::delta(100, 17);
330355
let neighbours = [Some(&nb_north), None, None, None];
331-
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default());
356+
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default(), None);
332357
assert_eq!(leaf.mode, CellMode::Merge);
333358
assert_eq!(leaf.merge_dir, Some(MergeDir::North));
334359
assert_eq!(leaf.basin_idx, 100);
@@ -340,7 +365,7 @@ mod tests {
340365
// reference frame). Falls through to Delta.
341366
let nb_north = LeafCu::delta(99, 17);
342367
let neighbours = [Some(&nb_north), None, None, None];
343-
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default());
368+
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default(), None);
344369
assert_eq!(leaf.mode, CellMode::Delta);
345370
}
346371

@@ -351,55 +376,86 @@ mod tests {
351376
let nb_merge = LeafCu::merge(100, MergeDir::North);
352377
let nb_esc = LeafCu::escape(100, 0);
353378
let neighbours = [Some(&nb_skip), Some(&nb_merge), None, Some(&nb_esc)];
354-
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default());
379+
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default(), None);
355380
assert_eq!(leaf.mode, CellMode::Delta);
356381
}
357382

358383
#[test]
359-
fn merge_picks_first_hit_in_nesw_order() {
384+
fn merge_picks_first_hit_in_news_order() {
360385
// Both N and E qualify; encoder must pick N (lower index).
361386
let nb_match = LeafCu::delta(100, 17);
362387
let neighbours = [Some(&nb_match), Some(&nb_match), None, None];
363-
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default());
388+
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default(), None);
364389
assert_eq!(leaf.merge_dir, Some(MergeDir::North));
365390
}
366391

392+
#[test]
393+
fn merge_slot_2_maps_to_west_and_slot_3_to_south() {
394+
// Slot-3 South coverage gap noted in review. Verify the
395+
// discriminant order (N=0, E=1, W=2, S=3) is reflected at
396+
// the merge_dir output, not just NEWS-by-convention.
397+
let nb = LeafCu::delta(100, 17);
398+
399+
let only_west = [None, None, Some(&nb), None];
400+
let leaf_w = predict_intra(&ctx_with_neighbours(100, 17, only_west), &IntraConfig::default(), None);
401+
assert_eq!(leaf_w.merge_dir, Some(MergeDir::West));
402+
403+
let only_south = [None, None, None, Some(&nb)];
404+
let leaf_s = predict_intra(&ctx_with_neighbours(100, 17, only_south), &IntraConfig::default(), None);
405+
assert_eq!(leaf_s.merge_dir, Some(MergeDir::South));
406+
}
407+
367408
#[test]
368409
fn merge_negative_delta_via_wrapping_cast() {
369410
// δ = -17 packs to 0xEF (= 239 as u8). Neighbour stored as
370411
// u8 = 0xEF MUST match — the cast must be wrapping, not
371412
// saturating.
372413
let nb_match = LeafCu::delta(100, (-17_i32) as u8);
373414
let neighbours = [None, Some(&nb_match), None, None];
374-
let leaf = predict_intra(&ctx_with_neighbours(100, -17, neighbours), &IntraConfig::default());
415+
let leaf = predict_intra(&ctx_with_neighbours(100, -17, neighbours), &IntraConfig::default(), None);
375416
assert_eq!(leaf.mode, CellMode::Merge);
376417
assert_eq!(leaf.merge_dir, Some(MergeDir::East));
377418
}
378419

379420
#[test]
380421
fn escape_when_delta_overflows_i8_and_allocator_present() {
381-
let cfg = IntraConfig {
382-
escape_next_idx: Some(42),
383-
};
384-
let leaf = predict_intra(&ctx_with_neighbours(100, 1000, [None; 4]), &cfg);
422+
let mut next = 42u32;
423+
let leaf = predict_intra(&ctx_with_neighbours(100, 1000, [None; 4]), &IntraConfig::default(), Some(&mut next));
385424
assert_eq!(leaf.mode, CellMode::Escape);
386425
assert_eq!(leaf.escape_idx, Some(42));
387426
assert_eq!(leaf.basin_idx, 100);
427+
// Cursor advanced so the next Escape gets a fresh idx.
428+
assert_eq!(next, 43);
429+
}
430+
431+
#[test]
432+
fn escape_allocator_advances_across_batched_calls() {
433+
// Regression: two consecutive Escape decisions must not
434+
// collide on the same vector slot. With a `&mut u32` cursor
435+
// the kernel post-increments, so cell A sees idx N and
436+
// cell B sees idx N+1.
437+
let mut next = 5u32;
438+
let a = predict_intra(&ctx_with_neighbours(7, 999, [None; 4]), &IntraConfig::default(), Some(&mut next));
439+
let b = predict_intra(&ctx_with_neighbours(7, -999, [None; 4]), &IntraConfig::default(), Some(&mut next));
440+
assert_eq!(a.escape_idx, Some(5));
441+
assert_eq!(b.escape_idx, Some(6));
442+
assert_eq!(next, 7);
443+
assert_ne!(a.escape_idx, b.escape_idx);
388444
}
389445

390446
#[test]
391447
fn escape_lossy_fallback_when_no_allocator() {
392448
// Without an escape_next_idx, the encoder clamps to i8 range.
393449
// The result is a valid LeafCu but the reconstruction won't
394450
// be bit-exact.
395-
let leaf = predict_intra(&ctx_with_neighbours(100, 1000, [None; 4]), &IntraConfig::default());
451+
let leaf = predict_intra(&ctx_with_neighbours(100, 1000, [None; 4]), &IntraConfig::default(), None);
396452
assert_eq!(leaf.mode, CellMode::Delta);
397453
assert_eq!(leaf.delta, Some(127));
398454
}
399455

400456
#[test]
401457
fn escape_lossy_fallback_negative_overflow() {
402-
let leaf = predict_intra(&ctx_with_neighbours(100, -1000, [None; 4]), &IntraConfig::default());
458+
let leaf = predict_intra(&ctx_with_neighbours(100, -1000, [None; 4]), &IntraConfig::default(), None);
403459
assert_eq!(leaf.mode, CellMode::Delta);
404460
assert_eq!(leaf.delta, Some((-128_i32) as u8));
405461
}
@@ -412,7 +468,7 @@ mod tests {
412468
use super::super::mode::{pack_leaf, unpack_leaf};
413469
let nb = LeafCu::delta(100, 17);
414470
let neighbours = [None, Some(&nb), None, None];
415-
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default());
471+
let leaf = predict_intra(&ctx_with_neighbours(100, 17, neighbours), &IntraConfig::default(), None);
416472
assert_eq!(leaf.mode, CellMode::Merge);
417473

418474
let mut buf = [0u8; 6];
@@ -438,7 +494,7 @@ mod tests {
438494
// clamp fallback because no allocator is wired).
439495
let nb_alias = LeafCu::delta(100, 0xC8);
440496
let neighbours = [Some(&nb_alias), None, None, None];
441-
let leaf = predict_intra(&ctx_with_neighbours(100, 200, neighbours), &IntraConfig::default());
497+
let leaf = predict_intra(&ctx_with_neighbours(100, 200, neighbours), &IntraConfig::default(), None);
442498
assert_ne!(leaf.mode, CellMode::Merge, "overflow δ must not Merge");
443499
// With no allocator the encoder clamps to +127 (lossy Delta).
444500
assert_eq!(leaf.mode, CellMode::Delta);
@@ -449,12 +505,11 @@ mod tests {
449505
fn overflow_delta_with_allocator_takes_escape() {
450506
let nb_alias = LeafCu::delta(100, 0xC8);
451507
let neighbours = [Some(&nb_alias), None, None, None];
452-
let cfg = IntraConfig {
453-
escape_next_idx: Some(7),
454-
};
455-
let leaf = predict_intra(&ctx_with_neighbours(100, 200, neighbours), &cfg);
508+
let mut next = 7u32;
509+
let leaf = predict_intra(&ctx_with_neighbours(100, 200, neighbours), &IntraConfig::default(), Some(&mut next));
456510
assert_eq!(leaf.mode, CellMode::Escape);
457511
assert_eq!(leaf.escape_idx, Some(7));
512+
assert_eq!(next, 8);
458513
}
459514

460515
#[test]

0 commit comments

Comments
 (0)