Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Working agreement — congee-wt

The operating contract for **any** coding agent working in this repository. Codex, Cursor
and Gemini CLI read `AGENTS.md` natively; Claude Code loads it through the `@AGENTS.md`
import in [`CLAUDE.md`](CLAUDE.md). Never fork these rules into a per-vendor file.

**Rust** concurrent index, a WorkTable dependency.

## Invariants (do not break these)

- **No Python.** Not a script, not `python3 -c`, not a heredoc. Reaching for it is the
tell that a step is being solved by parsing when the tool that owns the answer could
just be asked. Do not swap it for another parser either, and do not assume `jq` is
present: it does not ship with macOS. A fixed-shape field is one `sed -nE` line;
anything needing real parsing belongs in this repo's own language, where it can be
tested. If a task seems to need Python, the approach is wrong.

- **The Python still here is on its way out, not a precedent.** It predates the rule and
is deleted on sight when the owner finds it, so do not add to it, do not import from it,
and do not copy its approach into something new. If you are already changing one of
these, port it out rather than editing it in place:
- `.github/workflows/shumai_converter.py` (benchmark output conversion)
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@AGENTS.md

# Claude Code notes — congee-wt

The import above is binding: [`AGENTS.md`](AGENTS.md) is the working agreement for this
repository, and every Claude Code session loads it automatically. Do not copy rules here,
one source of truth, no drift. Only genuinely Claude-specific wiring belongs below.
11 changes: 8 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "congee-wt"
version = "0.4.4"
version = "0.4.5"
edition = "2024"
description = "A Rust implementation of ART-OLC concurrent adaptive radix tree."
keywords = ["ART", "adaptive-radix-tree", "concurrent"]
Expand All @@ -19,8 +19,8 @@ path = "src/lib.rs"
members = ["fuzz"]

[dependencies]
ps-reclaim = { version = "^0.1" }
serde = { version = "1.0.219", features = ["derive"], optional = true }
ps-reclaim = { version = "0.1.4", default-features = false, features = ["libc", "spin"] }
serde = { version = "1.0.219", default-features = false, features = ["derive", "alloc"], optional = true }

[dev-dependencies]
tracing-subscriber = "0.3"
Expand Down Expand Up @@ -56,6 +56,11 @@ path = "bench/compact_set.rs"
harness = false

[features]
default = ["std"]
# Off, this crate does not link `std`. What it costs: `debug_print`, which needs
# somewhere to print to, and `thread::yield_now` in the spin backoff, which
# falls back to a spin because there is no scheduler to yield to.
std = ["ps-reclaim/std", "serde?/std"]
flamegraph = ["shumai/flamegraph"]
perf = ["shumai/perf"]
stats = ["serde"]
Expand Down
6 changes: 5 additions & 1 deletion src/congee.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use std::{marker::PhantomData, ptr::with_exposed_provenance, sync::Arc};
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::marker::PhantomData;
use core::ptr::with_exposed_provenance;

use crate::{CongeeInner, DefaultAllocator, epoch, error::OOMError};

Expand Down
16 changes: 9 additions & 7 deletions src/congee_compact_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
//!
//! The structure is created by converting from a `CongeeSet` using `to_compact_set()`.

use std::marker::PhantomData;
use core::marker::PhantomData;

pub struct NodeType(pub u8);

Expand Down Expand Up @@ -226,8 +226,8 @@ impl CompactSetStats {
}
}

impl std::fmt::Display for CompactSetStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for CompactSetStats {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(
f,
"╭─────────────────────────────────────────────────────────────────╮"
Expand Down Expand Up @@ -421,7 +421,7 @@ where
data: &'a [u8],
_phantom: PhantomData<K>,
#[cfg(feature = "access-stats")]
access_stats: std::sync::Arc<std::sync::Mutex<AccessStats>>,
access_stats: alloc::sync::Arc<std::sync::Mutex<AccessStats>>,
}

#[cfg(feature = "access-stats")]
Expand Down Expand Up @@ -470,7 +470,7 @@ where
data,
_phantom: PhantomData,
#[cfg(feature = "access-stats")]
access_stats: std::sync::Arc::new(std::sync::Mutex::new(AccessStats::default())),
access_stats: alloc::sync::Arc::new(std::sync::Mutex::new(AccessStats::default())),
}
}

Expand Down Expand Up @@ -512,7 +512,7 @@ where
node_type: u8,
) -> Option<usize> {
unsafe {
use std::arch::x86_64::{
use core::arch::x86_64::{
_mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8, _mm_set1_epi8,
};

Expand Down Expand Up @@ -670,7 +670,7 @@ where

// Read bitmap
let bitmap = unsafe {
std::slice::from_raw_parts(self.data.as_ptr().add(bitmap_start), 32)
core::slice::from_raw_parts(self.data.as_ptr().add(bitmap_start), 32)
};
let bitmap_array = unsafe { *(bitmap.as_ptr() as *const [u8; 32]) };

Expand Down Expand Up @@ -832,6 +832,8 @@ where
}

/// Print the compact set in a human readable format
/// Needs somewhere to print to, which a `no_std` build does not have.
#[cfg(feature = "std")]
pub fn debug_print(&self) {
println!("\n=== CongeeCompactSet Debug Structure ===");
println!("Total nodes: {}", self.node_count());
Expand Down
15 changes: 9 additions & 6 deletions src/congee_inner.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::{marker::PhantomData, ptr::NonNull, sync::Arc};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::marker::PhantomData;
use core::ptr::NonNull;

use crate::{
Allocator, DefaultAllocator, cast_ptr,
Expand All @@ -9,10 +12,10 @@ use crate::{
range_scan::RangeScan,
utils::{Backoff, KeyTracker},
};
#[cfg(not(all(feature = "shuttle", test)))]
use core::sync::atomic::AtomicPtr;
#[cfg(all(feature = "shuttle", test))]
use shuttle::sync::atomic::AtomicPtr;
#[cfg(not(all(feature = "shuttle", test)))]
use std::sync::atomic::AtomicPtr;

/// Raw interface to the ART tree.
/// The `Art` is a wrapper around the `RawArt` that provides a safe interface.
Expand Down Expand Up @@ -112,7 +115,7 @@ impl<const K_LEN: usize, A: Allocator + Clone + Send> CongeeInner<K_LEN, A> {

#[inline]
pub(crate) fn load_root(&self) -> NonNull<BaseNode> {
let root_ptr = self.root.load(std::sync::atomic::Ordering::Relaxed);
let root_ptr = self.root.load(core::sync::atomic::Ordering::Relaxed);
// SAFETY: The root pointer is always non-null after initialization.
unsafe { NonNull::new_unchecked(root_ptr) }
}
Expand Down Expand Up @@ -612,7 +615,7 @@ impl<const K_LEN: usize, A: Allocator + Clone + Send> CongeeInner<K_LEN, A> {
let allocator = self.allocator.clone();
guard.defer(move || unsafe {
let ptr = NonNull::from(write_n.as_mut());
std::mem::forget(write_n);
core::mem::forget(write_n);
BaseNode::drop_node(ptr, allocator);
});
} else {
Expand Down Expand Up @@ -661,7 +664,7 @@ impl<const K_LEN: usize, A: Allocator + Clone + Send> CongeeInner<K_LEN, A> {

pub(crate) fn to_compact_set(&self) -> Vec<u8> {
use crate::congee_compact_set::NodeType as CompactNodeType;
use std::collections::VecDeque;
use alloc::collections::VecDeque;

let _pin = self.pin();

Expand Down
6 changes: 4 additions & 2 deletions src/congee_raw.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use std::{marker::PhantomData, sync::Arc};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::marker::PhantomData;

use crate::{Allocator, CongeeInner, DefaultAllocator, epoch, error::OOMError, stats};

Expand Down Expand Up @@ -233,7 +235,7 @@ where
let start: [u8; 8] = start.to_be_bytes();
let end: [u8; 8] = end.to_be_bytes();
let result_ref = unsafe {
std::slice::from_raw_parts_mut(
core::slice::from_raw_parts_mut(
result.as_mut_ptr() as *mut ([u8; 8], usize),
result.len(),
)
Expand Down
5 changes: 4 additions & 1 deletion src/congee_set.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::{marker::PhantomData, sync::Arc};
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::marker::PhantomData;

use crate::{Allocator, CongeeInner, DefaultAllocator, epoch, error::OOMError, stats};

Expand Down
4 changes: 2 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::error::Error;
use std::fmt::{self, Debug, Display, Formatter};
use core::error::Error;
use core::fmt::{self, Debug, Display, Formatter};

#[derive(Debug)]
pub(crate) enum ArtError {
Expand Down
14 changes: 11 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
#![cfg_attr(not(any(test, feature = "std")), no_std)]
#![doc = include_str!("../README.md")]
#![allow(clippy::comparison_chain)]
#![allow(clippy::len_without_is_empty)]
#![cfg_attr(docsrs, feature(doc_cfg))]

// The crate does not link `std` unless asked. Tests always do: they spawn
// threads and time things, which is what a concurrency test is made of.
#[cfg(any(test, feature = "std"))]
extern crate std;

extern crate alloc;

mod congee;
pub mod congee_compact_set;
mod congee_inner;
Expand All @@ -22,7 +30,7 @@ mod tests;

/// Types needed to safely access shared data concurrently.
pub mod epoch {
use std::sync::atomic::{AtomicUsize, Ordering};
use core::sync::atomic::{AtomicUsize, Ordering};

const RECLAIM_BATCH: usize = 256;

Expand Down Expand Up @@ -87,7 +95,7 @@ pub mod epoch {
}

pub(crate) fn belongs_to(&self, reclaimer: &Reclaimer) -> bool {
std::ptr::eq(self.reclaimer, reclaimer)
core::ptr::eq(self.reclaimer, reclaimer)
}
}

Expand All @@ -102,8 +110,8 @@ pub mod epoch {
#[cfg(test)]
mod tests {
use super::{RECLAIM_BATCH, Reclaimer, pin_in};
use core::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

#[test]
fn automatic_collection_is_bounded_to_one_batch() {
Expand Down
6 changes: 4 additions & 2 deletions src/lock.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::{marker::PhantomData, ptr::NonNull, sync::atomic::Ordering};
use core::marker::PhantomData;
use core::ptr::NonNull;
use core::sync::atomic::Ordering;

#[cfg(all(feature = "shuttle", test))]
use shuttle::sync::atomic::fence;

#[cfg(not(all(feature = "shuttle", test)))]
use std::sync::atomic::fence;
use core::sync::atomic::fence;

use crate::{
error::ArtError,
Expand Down
47 changes: 24 additions & 23 deletions src/nodes/base_node.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use alloc::vec::Vec;
use core::ptr::NonNull;
#[cfg(not(all(feature = "shuttle", test)))]
use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
#[cfg(all(feature = "shuttle", test))]
use shuttle::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
use std::ptr::NonNull;
#[cfg(not(all(feature = "shuttle", test)))]
use std::sync::atomic::{AtomicPtr, AtomicU32, Ordering};

use crate::{
Allocator,
Expand Down Expand Up @@ -49,26 +50,26 @@ impl NodeType {
}
}

pub(crate) fn node_layout(&self) -> std::alloc::Layout {
pub(crate) fn node_layout(&self) -> core::alloc::Layout {
match *self {
NodeType::N4 => std::alloc::Layout::from_size_align(
std::mem::size_of::<Node4>(),
std::mem::align_of::<Node4>(),
NodeType::N4 => core::alloc::Layout::from_size_align(
core::mem::size_of::<Node4>(),
core::mem::align_of::<Node4>(),
)
.unwrap(),
NodeType::N16 => std::alloc::Layout::from_size_align(
std::mem::size_of::<Node16>(),
std::mem::align_of::<Node16>(),
NodeType::N16 => core::alloc::Layout::from_size_align(
core::mem::size_of::<Node16>(),
core::mem::align_of::<Node16>(),
)
.unwrap(),
NodeType::N48 => std::alloc::Layout::from_size_align(
std::mem::size_of::<Node48>(),
std::mem::align_of::<Node48>(),
NodeType::N48 => core::alloc::Layout::from_size_align(
core::mem::size_of::<Node48>(),
core::mem::align_of::<Node48>(),
)
.unwrap(),
NodeType::N256 => std::alloc::Layout::from_size_align(
std::mem::size_of::<Node256>(),
std::mem::align_of::<Node256>(),
NodeType::N256 => core::alloc::Layout::from_size_align(
core::mem::size_of::<Node256>(),
core::mem::align_of::<Node256>(),
)
.unwrap(),
}
Expand Down Expand Up @@ -148,10 +149,10 @@ impl NodeMeta {
#[cfg(not(feature = "shuttle"))]
mod layout_assertion {
use super::*;
const _: () = assert!(std::mem::size_of::<NodeMeta>() == 12);
const _: () = assert!(std::mem::align_of::<NodeMeta>() == 2);
const _: () = assert!(std::mem::size_of::<BaseNode>() == 16);
const _: () = assert!(std::mem::align_of::<BaseNode>() == 4);
const _: () = assert!(core::mem::size_of::<NodeMeta>() == 12);
const _: () = assert!(core::mem::align_of::<NodeMeta>() == 2);
const _: () = assert!(core::mem::size_of::<BaseNode>() == 16);
const _: () = assert!(core::mem::align_of::<BaseNode>() == 4);
}

macro_rules! gen_method {
Expand Down Expand Up @@ -251,7 +252,7 @@ impl BaseNode {
let base_ptr = ptr.as_ptr() as *mut BaseNode;
let node = BaseNode::new(N::get_type(), prefix);
unsafe {
std::ptr::write(base_ptr, node);
core::ptr::write(base_ptr, node);

if matches!(N::get_type(), NodeType::N48) {
let mem = base_ptr as *mut Node48;
Expand Down Expand Up @@ -288,7 +289,7 @@ impl BaseNode {
/// Here we must get a clone of allocator because the drop_node might be called in epoch guard
pub(crate) unsafe fn drop_node<A: Allocator>(node: NonNull<BaseNode>, allocator: A) {
let layout = unsafe { node.as_ref() }.get_type().node_layout();
let ptr = std::ptr::NonNull::new(node.as_ptr() as *mut u8).unwrap();
let ptr = core::ptr::NonNull::new(node.as_ptr() as *mut u8).unwrap();
unsafe {
allocator.deallocate(ptr, layout);
}
Expand Down Expand Up @@ -422,7 +423,7 @@ impl BaseNode {

write_n.mark_obsolete();
let delete_n = write_n.as_mut() as *mut CurT as usize;
std::mem::forget(write_n);
core::mem::forget(write_n);
let allocator: A = allocator.clone();
guard.defer(move || unsafe {
let delete_n = NonNull::new(delete_n as *mut BaseNode).unwrap();
Expand Down
7 changes: 4 additions & 3 deletions src/nodes/node_16.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use super::{
NodePtr,
base_node::{BaseNode, Node, NodeIter, NodeType},
};
use alloc::vec::Vec;

#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
use core::arch::x86_64::*;

#[repr(C)]
#[repr(align(8))] // Node 16 doesn't need to align to 64 bc it occupies 3 cache lines anyway
Expand All @@ -15,10 +16,10 @@ pub(crate) struct Node16 {
}

#[cfg(not(feature = "shuttle"))]
const _: () = assert!(std::mem::size_of::<Node16>() == 160);
const _: () = assert!(core::mem::size_of::<Node16>() == 160);

#[cfg(not(feature = "shuttle"))]
const _: () = assert!(std::mem::align_of::<Node16>() == 8);
const _: () = assert!(core::mem::align_of::<Node16>() == 8);

impl Node16 {
fn get_insert_pos(&self, key: u8) -> usize {
Expand Down
Loading
Loading