diff --git a/src/line_list_node.rs b/src/line_list_node.rs index 040f714..b12fb2a 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -339,6 +339,9 @@ impl LineListNode { const LEN_MASK: u16 = 0xfc0; //bits 11 to 6, inclusive self.header &= !LEN_MASK; self.header |= (new_len << 6) as u16; + if !self.is_used::<1>() && new_len < KEY_BYTES_CNT { + self.header &= !(1u16 << 12); + } if self.is_used::<1>() { let key_len_1 = self.key_len_1(); unsafe { @@ -2541,8 +2544,7 @@ impl TrieNode for LineListNode let dst_ptr = base_ptr; core::ptr::copy(src_ptr, dst_ptr, new_key_len); } - temp_node.header &= 0xf03f; //Zero out the old length, and reset it - temp_node.header |= (new_key_len << 6) as u16; + temp_node.shorten_key_len::<0>(new_key_len); debug_assert!(validate_node(&temp_node)); return Some(TrieNodeODRc::new_in(temp_node, self.alloc.clone())) } else { @@ -2771,6 +2773,15 @@ pub(crate) fn validate_node(node: &LineLis panic!() } + if !node.is_used::<1>() { + let slot_1_unavailable = node.header & (1 << 12) > 0; + let expected_unavailable = node.is_used::<0>() && key0.len() == KEY_BYTES_CNT; + if slot_1_unavailable != expected_unavailable { + println!("Invalid node - stale slot_1 availability bit. {node:?}"); + panic!() + } + } + //key0 must always be alphabetically before key1, if slot_1 is filled if node.is_used::<1>() && key0 > key1 { println!("Invalid node - keys not sorted {node:?}"); @@ -2904,6 +2915,51 @@ mod tests { assert_eq!(new_node.node_get_val("hello".as_bytes()), Some(&42)); } + /// Regression for a stale "slot_1 unavailable" bit after shortening a full-width slot_0 key. + #[test] + fn test_line_list_shorten_key_releases_slot_1_availability() { + let full_key = vec![b'a'; KEY_BYTES_CNT]; + let prefix_len = (KEY_BYTES_CNT / 3).max(1); + debug_assert!(prefix_len < KEY_BYTES_CNT); + let prefix = vec![b'a'; prefix_len]; + let suffix = &full_key[prefix.len()..]; + + let mut new_node = LineListNode::::new_in(global_alloc()); + assert_eq!(new_node.node_set_val(&full_key, 24).map_err(|_| 0), Ok((None, false))); + + let detached = new_node.take_node_at_key(&prefix, false).unwrap(); + assert_eq!(detached.as_tagged().node_get_val(suffix), Some(&24)); + + assert_eq!(new_node.key_len_0(), prefix.len()); + assert!(!new_node.is_used::<1>()); + assert!( + new_node.is_available_1(), + "slot_1 should become available after shortening slot_0 below KEY_BYTES_CNT" + ); + + assert_eq!(new_node.node_set_val(b"z", 42).map_err(|_| 0), Ok((None, false))); + assert_eq!(new_node.node_get_val(b"z"), Some(&42)); + } + + /// Regression for the single-slot drop_head_dyn path preserving a stale slot_1-unavailable bit. + #[test] + fn test_line_list_drop_head_releases_slot_1_availability() { + let full_key = vec![b'a'; KEY_BYTES_CNT]; + let drop_bytes = (KEY_BYTES_CNT / 3).max(1); + let expected_key_len = KEY_BYTES_CNT - drop_bytes; + + let mut new_node = LineListNode::::new_in(global_alloc()); + assert_eq!(new_node.node_set_val(&full_key, 24).map_err(|_| 0), Ok((None, false))); + + let mut shortened = new_node.drop_head_dyn(drop_bytes).unwrap().as_tagged().as_list().unwrap().clone(); + assert_eq!(shortened.key_len_0(), expected_key_len); + assert!(!shortened.is_used::<1>()); + assert!(shortened.is_available_1()); + + assert_eq!(shortened.node_set_val(b"z", 42).map_err(|_| 0), Ok((None, false))); + assert_eq!(shortened.node_get_val(b"z"), Some(&42)); + } + /// This tests that a common prefix is found with the entry in slot_0, when slot_1 is already full #[test] fn test_line_list_node_shared_prefixes_slot_0() { @@ -3337,4 +3393,4 @@ mod tests { // and ZipperMoving // 2. implement a val_count convenience on top of 1. -//GOAT, Paths in caching Cata: https://github.com/Adam-Vandervorst/PathMap/pull/8#discussion_r2004828957 \ No newline at end of file +//GOAT, Paths in caching Cata: https://github.com/Adam-Vandervorst/PathMap/pull/8#discussion_r2004828957