Skip to content

Commit b27fd47

Browse files
author
CID Agent
committed
cid(advance): optimize codec header decoding with direct bitwise operations
1 parent 1003e1e commit b27fd47

File tree

3 files changed

+157
-69
lines changed

3 files changed

+157
-69
lines changed

.claude/agent-memory/advance/MEMORY.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,19 @@ iterations.
163163
"Runtime setup", streaming uses opaque `long` handles with try-finally (not defer),
164164
`genIsccCodeV0` exposes `boolean wide` parameter (Go hardcodes to false)
165165

166+
## Codec Internals
167+
168+
- `decode_header` and `decode_varnibble_from_bytes` operate directly on `&[u8]` with bitwise
169+
extraction — no intermediate `Vec<bool>` allocation. Helpers: `get_bit(data, bit_pos)` reads a
170+
single bit, `extract_bits(data, bit_pos, count)` reads N bits as u32 (both MSB-first)
171+
- `encode_header` and `encode_varnibble` still use `Vec<bool>` internally (encode path is less
172+
performance-sensitive)
173+
- `bytes_to_bits` and `bits_to_u32` are `#[cfg(test)]` only — used by test helpers but not
174+
production code
175+
- `bits_to_bytes` is still in production code (used by `encode_header`)
176+
- Rust 1.93+ clippy lints: `collapsible_if` and `manual_div_ceil` (use `n.div_ceil(d)` instead of
177+
`(n + d - 1) / d`)
178+
166179
## Gotchas
167180

168181
- `pop_local_frame` is `unsafe` in jni crate v0.21 (Rust 2024 edition) — must wrap in `unsafe {}`

.claude/context/handoff.md

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,29 @@
1-
## 2026-02-25 — Review of: Implement Java NativeLoader class
2-
3-
**Verdict:** PASS
4-
5-
**Summary:** Created `NativeLoader.java` with platform detection, JAR resource extraction, temp file
6-
management, and `System.loadLibrary` fallback. Updated `IsccLib.java` to delegate to
7-
`NativeLoader.load()`. Clean implementation following the well-established JNI native loader pattern
8-
(sqlite-jdbc, netty-tcnative). All verification criteria met.
9-
10-
**Verification:**
11-
12-
- [x] `cd crates/iscc-jni && cargo build` succeeds — Rust side unchanged, compiles clean
13-
- [x] `cd crates/iscc-jni/java && mvn test` — all 49 tests pass (loader fallback path works)
14-
- [x] `test -f NativeLoader.java` exits 0 — file exists at expected path
15-
- [x] `grep 'NativeLoader.load' IsccLib.java` exits 0 — delegation present in static initializer
16-
- [x] `grep -c 'System.loadLibrary' IsccLib.java` outputs `0` — no direct loadLibrary in IsccLib
17-
- [x] `grep 'META-INF/native' NativeLoader.java` exits 0 — extraction path present (4 matches)
18-
- [x] All 14 pre-commit hooks pass
19-
- [x] No quality gate circumvention in diff
20-
21-
**Issues found:**
22-
23-
- (none)
24-
25-
**Next:** The NativeLoader infrastructure is in place. The JAR extraction path won't activate until
26-
native binaries are bundled into `META-INF/native/` during CI (a future iteration). Consider
27-
addressing tracked issues: codec header parsing optimization (`[normal]`), DataHasher buffer
28-
allocation (`[normal]`), iscc-ffi video frame allocation (`[normal]`), or lower-priority items like
29-
`__version__` attribute, module docstring fix, WASM conformance feature gate, DCT power-of-two
30-
validation, or the JNI `IllegalStateException` improvement.
31-
32-
**Notes:** Helper methods (`detectOs`, `detectArch`, `libraryFileName`) are package-private rather
33-
than private, making them testable without reflection. The `try (stream)` pattern uses Java 9+
34-
try-with-resources on an effectively-final variable, consistent with the Java 17 target. The
35-
`volatile` + `synchronized` pattern is a correct (if conservative) thread-safety approach — the
36-
`synchronized` on `load()` makes the volatile guard redundant for correctness but harmless and
37-
documents intent clearly.
1+
## 2026-02-25 — Optimize codec header decoding with direct bitwise operations
2+
3+
**Done:** Replaced `Vec<bool>` intermediate allocation in `decode_header` and `decode_varnibble`
4+
with direct bitwise extraction from the byte slice. Added `get_bit` and `extract_bits` helpers that
5+
operate on `&[u8]` + bit position. Gated `bytes_to_bits` and `bits_to_u32` with `#[cfg(test)]` since
6+
they're only used by test helpers after the rewrite.
7+
8+
**Files changed:**
9+
10+
- `crates/iscc-lib/src/codec.rs`: Added `get_bit`, `extract_bits`, `decode_varnibble_from_bytes`
11+
functions; rewrote `decode_header` to use byte-based decoding; gated `bytes_to_bits` and
12+
`bits_to_u32` with `#[cfg(test)]`; updated `test_varnibble_roundtrip` to use new function; added
13+
`test_extract_bits_basic` and `test_decode_varnibble_from_bytes_boundary_values` tests
14+
15+
**Verification:** All 261 tests pass (208 unit + 31 algo primitives + 22 text utils). Clippy clean
16+
(workspace-wide `--all-targets`). All 14 pre-commit hooks pass. Benchmark shows 27% improvement for
17+
`gen_mixed_code_v0` (1.34µs → was ~1.87µs). Benchmark runs without error.
18+
19+
**Next:** The codec issue (`[normal] Codec header parsing expands bytes to Vec<bool>`) is now
20+
resolved and can be deleted from issues.md. Consider addressing `DataHasher::update` buffer
21+
allocation (`[normal]`), iscc-ffi video frame allocation (`[normal]`), or lower-priority items.
22+
23+
**Notes:** The old `decode_varnibble` function was removed entirely (no production or test callers
24+
remain after updating `test_varnibble_roundtrip`). Clippy suggested two improvements: collapsing the
25+
nested `if` in the padding check into a single condition, and using `bit_pos.div_ceil(8)` instead of
26+
`(bit_pos + 7) / 8`. Both applied. The tail extraction behavior is slightly cleaner for invalid data
27+
(non-zero padding) — it now rounds up to the next byte boundary instead of reconstructing mid-byte
28+
tail data through `bits_to_bytes`, but this difference is invisible for valid ISCC data where
29+
padding is always zero.

crates/iscc-lib/src/codec.rs

Lines changed: 115 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,30 @@ impl TryFrom<u8> for Version {
113113

114114
// ---- Bit Manipulation Helpers ----
115115

116+
/// Read bit at position `bit_pos` from byte slice (MSB-first ordering).
117+
fn get_bit(data: &[u8], bit_pos: usize) -> bool {
118+
let byte_idx = bit_pos / 8;
119+
let bit_idx = 7 - (bit_pos % 8);
120+
(data[byte_idx] >> bit_idx) & 1 == 1
121+
}
122+
123+
/// Extract `count` bits starting at `bit_pos` as a u32 (MSB-first).
124+
fn extract_bits(data: &[u8], bit_pos: usize, count: usize) -> u32 {
125+
let mut value = 0u32;
126+
for i in 0..count {
127+
value = (value << 1) | u32::from(get_bit(data, bit_pos + i));
128+
}
129+
value
130+
}
131+
116132
/// Convert a bit slice (big-endian, MSB first) to a u32.
133+
#[cfg(test)]
117134
fn bits_to_u32(bits: &[bool]) -> u32 {
118135
bits.iter().fold(0u32, |acc, &b| (acc << 1) | u32::from(b))
119136
}
120137

121138
/// Convert bytes to a bit vector (big-endian, MSB first).
139+
#[cfg(test)]
122140
fn bytes_to_bits(bytes: &[u8]) -> Vec<bool> {
123141
bytes
124142
.iter()
@@ -180,28 +198,31 @@ fn encode_varnibble(value: u32) -> IsccResult<Vec<bool>> {
180198
}
181199
}
182200

183-
/// Decode the first varnibble from a bit slice.
201+
/// Decode the first varnibble from a byte slice at the given bit position.
184202
///
185-
/// Returns the decoded integer and the number of bits consumed.
186-
fn decode_varnibble(bits: &[bool]) -> IsccResult<(u32, usize)> {
187-
if bits.len() < 4 {
203+
/// Operates directly on `&[u8]` with bitwise extraction, avoiding any
204+
/// intermediate `Vec<bool>` allocation. Returns the decoded integer and
205+
/// the number of bits consumed.
206+
fn decode_varnibble_from_bytes(data: &[u8], bit_pos: usize) -> IsccResult<(u32, usize)> {
207+
let available = data.len() * 8 - bit_pos;
208+
if available < 4 {
188209
return Err(IsccError::InvalidInput(
189210
"insufficient bits for varnibble".into(),
190211
));
191212
}
192213

193-
if !bits[0] {
214+
if !get_bit(data, bit_pos) {
194215
// 0xxx — 4 bits, values 0–7
195-
Ok((bits_to_u32(&bits[..4]), 4))
196-
} else if bits.len() >= 8 && !bits[1] {
216+
Ok((extract_bits(data, bit_pos, 4), 4))
217+
} else if available >= 8 && !get_bit(data, bit_pos + 1) {
197218
// 10xxxxxx — 8 bits, values 8–71
198-
Ok((bits_to_u32(&bits[2..8]) + 8, 8))
199-
} else if bits.len() >= 12 && !bits[2] {
219+
Ok((extract_bits(data, bit_pos + 2, 6) + 8, 8))
220+
} else if available >= 12 && !get_bit(data, bit_pos + 2) {
200221
// 110xxxxxxxxx — 12 bits, values 72–583
201-
Ok((bits_to_u32(&bits[3..12]) + 72, 12))
202-
} else if bits.len() >= 16 && !bits[3] {
222+
Ok((extract_bits(data, bit_pos + 3, 9) + 72, 12))
223+
} else if available >= 16 && !get_bit(data, bit_pos + 3) {
203224
// 1110xxxxxxxxxxxx — 16 bits, values 584–4679
204-
Ok((bits_to_u32(&bits[4..16]) + 584, 16))
225+
Ok((extract_bits(data, bit_pos + 4, 12) + 584, 16))
205226
} else {
206227
Err(IsccError::InvalidInput(
207228
"invalid varnibble prefix or insufficient bits".into(),
@@ -239,35 +260,35 @@ pub fn encode_header(
239260

240261
/// Decode ISCC header from bytes.
241262
///
242-
/// Returns `(MainType, SubType, Version, length, tail_bytes)` where
243-
/// `tail_bytes` contains any remaining data after the header.
263+
/// Operates directly on `&[u8]` with bitwise extraction, avoiding any
264+
/// intermediate `Vec<bool>` allocation. Returns `(MainType, SubType,
265+
/// Version, length, tail_bytes)` where `tail_bytes` contains any
266+
/// remaining data after the header.
244267
pub fn decode_header(data: &[u8]) -> IsccResult<(MainType, SubType, Version, u32, Vec<u8>)> {
245-
let bits = bytes_to_bits(data);
246-
let mut pos = 0;
268+
let mut bit_pos = 0;
247269

248-
let (mtype_val, consumed) = decode_varnibble(&bits[pos..])?;
249-
pos += consumed;
270+
let (mtype_val, consumed) = decode_varnibble_from_bytes(data, bit_pos)?;
271+
bit_pos += consumed;
250272

251-
let (stype_val, consumed) = decode_varnibble(&bits[pos..])?;
252-
pos += consumed;
273+
let (stype_val, consumed) = decode_varnibble_from_bytes(data, bit_pos)?;
274+
bit_pos += consumed;
253275

254-
let (version_val, consumed) = decode_varnibble(&bits[pos..])?;
255-
pos += consumed;
276+
let (version_val, consumed) = decode_varnibble_from_bytes(data, bit_pos)?;
277+
bit_pos += consumed;
256278

257-
let (length, consumed) = decode_varnibble(&bits[pos..])?;
258-
pos += consumed;
279+
let (length, consumed) = decode_varnibble_from_bytes(data, bit_pos)?;
280+
bit_pos += consumed;
259281

260282
// Strip 4-bit zero padding if header bits are not byte-aligned.
261283
// Since each varnibble is a multiple of 4 bits, misalignment is always 4 bits.
262-
if pos % 8 != 0 {
263-
let pad_end = pos + 4;
264-
if pad_end <= bits.len() && bits[pos..pad_end].iter().all(|&b| !b) {
265-
pos = pad_end;
266-
}
284+
if bit_pos % 8 != 0 && bit_pos + 4 <= data.len() * 8 && extract_bits(data, bit_pos, 4) == 0 {
285+
bit_pos += 4;
267286
}
268287

269-
let tail = if pos < bits.len() {
270-
bits_to_bytes(&bits[pos..])
288+
// Advance to next byte boundary for tail extraction
289+
let tail_byte_start = bit_pos.div_ceil(8);
290+
let tail = if tail_byte_start < data.len() {
291+
data[tail_byte_start..].to_vec()
271292
} else {
272293
vec![]
273294
};
@@ -557,7 +578,8 @@ mod tests {
557578
let test_values = [0, 1, 7, 8, 71, 72, 583, 584, 4679];
558579
for &value in &test_values {
559580
let bits = encode_varnibble(value).unwrap();
560-
let (decoded, consumed) = decode_varnibble(&bits).unwrap();
581+
let bytes = bits_to_bytes(&bits);
582+
let (decoded, consumed) = decode_varnibble_from_bytes(&bytes, 0).unwrap();
561583
assert_eq!(decoded, value, "roundtrip failed for value {value}");
562584
assert_eq!(consumed, bits.len(), "consumed mismatch for value {value}");
563585
}
@@ -600,6 +622,67 @@ mod tests {
600622
); // 10 000000
601623
}
602624

625+
// ---- Bitwise extraction tests ----
626+
627+
#[test]
628+
fn test_extract_bits_basic() {
629+
// 0xA5 = 1010_0101 in binary
630+
let data = [0xA5u8];
631+
assert_eq!(extract_bits(&data, 0, 4), 0b1010); // first nibble
632+
assert_eq!(extract_bits(&data, 4, 4), 0b0101); // second nibble
633+
assert_eq!(extract_bits(&data, 0, 8), 0xA5); // full byte
634+
assert_eq!(extract_bits(&data, 1, 3), 0b010); // bits 1-3
635+
assert_eq!(extract_bits(&data, 0, 1), 1); // MSB
636+
assert_eq!(extract_bits(&data, 7, 1), 1); // LSB
637+
638+
// Multi-byte: 0xFF 0x00 = 1111_1111 0000_0000
639+
let data2 = [0xFF, 0x00];
640+
assert_eq!(extract_bits(&data2, 0, 8), 0xFF);
641+
assert_eq!(extract_bits(&data2, 8, 8), 0x00);
642+
assert_eq!(extract_bits(&data2, 4, 8), 0xF0); // crossing byte boundary
643+
assert_eq!(extract_bits(&data2, 6, 4), 0b1100); // crossing byte boundary
644+
}
645+
646+
#[test]
647+
fn test_decode_varnibble_from_bytes_boundary_values() {
648+
// Test decoding at non-zero bit offsets within a byte slice.
649+
// Encode two varnibbles into a single byte sequence and decode both.
650+
651+
// varnibble(3) = 0011 (4 bits) + varnibble(8) = 10_000000 (8 bits) = 12 bits
652+
let bits_3 = encode_varnibble(3).unwrap();
653+
let bits_8 = encode_varnibble(8).unwrap();
654+
let mut combined_bits = bits_3.clone();
655+
combined_bits.extend(&bits_8);
656+
let bytes = bits_to_bytes(&combined_bits);
657+
658+
// Decode first varnibble at bit 0
659+
let (val1, consumed1) = decode_varnibble_from_bytes(&bytes, 0).unwrap();
660+
assert_eq!(val1, 3);
661+
assert_eq!(consumed1, 4);
662+
663+
// Decode second varnibble at bit 4 (non-zero offset)
664+
let (val2, consumed2) = decode_varnibble_from_bytes(&bytes, 4).unwrap();
665+
assert_eq!(val2, 8);
666+
assert_eq!(consumed2, 8);
667+
668+
// Test with a 3-nibble value at offset
669+
// varnibble(0) = 0000 (4 bits) + varnibble(72) = 110_000000000 (12 bits)
670+
let bits_0 = encode_varnibble(0).unwrap();
671+
let bits_72 = encode_varnibble(72).unwrap();
672+
let mut combined2 = bits_0;
673+
combined2.extend(&bits_72);
674+
let bytes2 = bits_to_bytes(&combined2);
675+
676+
let (val3, consumed3) = decode_varnibble_from_bytes(&bytes2, 4).unwrap();
677+
assert_eq!(val3, 72);
678+
assert_eq!(consumed3, 12);
679+
680+
// Test insufficient bits at offset
681+
let single_byte = [0x00u8];
682+
let result = decode_varnibble_from_bytes(&single_byte, 6);
683+
assert!(result.is_err(), "should fail with only 2 bits available");
684+
}
685+
603686
// ---- Header encoding tests ----
604687

605688
#[test]

0 commit comments

Comments
 (0)