capnp_store: declarative serialization via macros

Replace 130 lines of manual field-by-field capnp serialization with
two declarative macros:

  capnp_enum!  — generates to_capnp/from_capnp for enum types
  capnp_message! — generates from_capnp/to_capnp for structs

Adding a field to the capnp schema now means adding it in one place;
both read and write directions are generated from the same declaration.

Eliminates: read_content_node, write_content_node, read_relation,
write_relation, read_provenance (5 functions → 2 macro invocations).

Callers updated to method syntax: Node::from_capnp() / node.to_capnp().

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
ProofOfConcept 2026-03-03 12:25:10 -05:00
parent ec8b4b2ed2
commit ea0d631051
3 changed files with 115 additions and 136 deletions

1
Cargo.lock generated
View file

@ -1018,6 +1018,7 @@ dependencies = [
"faer", "faer",
"libc", "libc",
"memmap2", "memmap2",
"paste",
"peg", "peg",
"rayon", "rayon",
"regex", "regex",

View file

@ -16,6 +16,7 @@ rkyv = { version = "0.7", features = ["validation", "std"] }
memmap2 = "0.9" memmap2 = "0.9"
rayon = "1" rayon = "1"
peg = "0.8" peg = "0.8"
paste = "1"
[build-dependencies] [build-dependencies]
capnpc = "0.20" capnpc = "0.20"

View file

@ -29,6 +29,77 @@ use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
// ---------------------------------------------------------------------------
// Capnp serialization macros
//
// Declarative mapping between Rust types and capnp generated types.
// Adding a field to the schema means adding it in one place below;
// both read and write are generated from the same declaration.
// ---------------------------------------------------------------------------
/// Generate to_capnp/from_capnp conversion methods for an enum.
macro_rules! capnp_enum {
($rust_type:ident, $capnp_type:path, [$($variant:ident),+ $(,)?]) => {
impl $rust_type {
fn to_capnp(&self) -> $capnp_type {
match self {
$(Self::$variant => <$capnp_type>::$variant,)+
}
}
fn from_capnp(v: $capnp_type) -> Self {
match v {
$(<$capnp_type>::$variant => Self::$variant,)+
}
}
}
};
}
/// Generate from_capnp/to_capnp methods for a struct with capnp serialization.
/// Fields are grouped by serialization kind:
/// text - capnp Text fields (String in Rust)
/// uuid - capnp Data fields ([u8; 16] in Rust)
/// prim - copy types (u32, f32, f64, bool)
/// enm - enums with to_capnp/from_capnp methods
/// skip - Rust-only fields not in capnp (set to Default on read)
macro_rules! capnp_message {
(
$struct:ident,
reader: $reader:ty,
builder: $builder:ty,
text: [$($tf:ident),* $(,)?],
uuid: [$($uf:ident),* $(,)?],
prim: [$($pf:ident),* $(,)?],
enm: [$($ef:ident: $et:ident),* $(,)?],
skip: [$($sf:ident),* $(,)?] $(,)?
) => {
impl $struct {
fn from_capnp(r: $reader) -> Result<Self, String> {
paste::paste! {
Ok(Self {
$($tf: read_text(r.[<get_ $tf>]()),)*
$($uf: read_uuid(r.[<get_ $uf>]()),)*
$($pf: r.[<get_ $pf>](),)*
$($ef: $et::from_capnp(
r.[<get_ $ef>]().map_err(|_| concat!("bad ", stringify!($ef)))?
),)*
$($sf: Default::default(),)*
})
}
}
fn to_capnp(&self, mut b: $builder) {
paste::paste! {
$(b.[<set_ $tf>](&self.$tf);)*
$(b.[<set_ $uf>](&self.$uf);)*
$(b.[<set_ $pf>](self.$pf);)*
$(b.[<set_ $ef>](self.$ef.to_capnp());)*
}
}
}
};
}
// Data dir: ~/.claude/memory/ // Data dir: ~/.claude/memory/
fn memory_dir() -> PathBuf { fn memory_dir() -> PathBuf {
PathBuf::from(env::var("HOME").expect("HOME not set")) PathBuf::from(env::var("HOME").expect("HOME not set"))
@ -234,6 +305,40 @@ pub enum RelationType {
Auto, Auto,
} }
capnp_enum!(NodeType, memory_capnp::NodeType,
[EpisodicSession, EpisodicDaily, EpisodicWeekly, Semantic]);
capnp_enum!(Provenance, memory_capnp::Provenance,
[Manual, Journal, Agent, Dream, Derived]);
capnp_enum!(Category, memory_capnp::Category,
[General, Core, Technical, Observation, Task]);
capnp_enum!(RelationType, memory_capnp::RelationType,
[Link, Causal, Auto]);
capnp_message!(Node,
reader: memory_capnp::content_node::Reader<'_>,
builder: memory_capnp::content_node::Builder<'_>,
text: [key, content, source_ref, created, state_tag],
uuid: [uuid],
prim: [version, timestamp, weight, emotion, deleted,
retrievals, uses, wrongs, last_replayed,
spaced_repetition_interval, position],
enm: [node_type: NodeType, provenance: Provenance, category: Category],
skip: [community_id, clustering_coefficient, degree],
);
capnp_message!(Relation,
reader: memory_capnp::relation::Reader<'_>,
builder: memory_capnp::relation::Builder<'_>,
text: [source_key, target_key],
uuid: [uuid, source, target],
prim: [version, timestamp, strength, deleted],
enm: [rel_type: RelationType, provenance: Provenance],
skip: [],
);
#[derive(Clone, Debug, Serialize, Deserialize, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[archive(check_bytes)] #[archive(check_bytes)]
pub struct RetrievalEvent { pub struct RetrievalEvent {
@ -614,7 +719,7 @@ impl Store {
.map_err(|e| format!("read node log: {}", e))?; .map_err(|e| format!("read node log: {}", e))?;
for node_reader in log.get_nodes() for node_reader in log.get_nodes()
.map_err(|e| format!("get nodes: {}", e))? { .map_err(|e| format!("get nodes: {}", e))? {
let node = read_content_node(node_reader)?; let node = Node::from_capnp(node_reader)?;
let existing_version = self.nodes.get(&node.key) let existing_version = self.nodes.get(&node.key)
.map(|n| n.version) .map(|n| n.version)
.unwrap_or(0); .unwrap_or(0);
@ -646,7 +751,7 @@ impl Store {
.map_err(|e| format!("read relation log: {}", e))?; .map_err(|e| format!("read relation log: {}", e))?;
for rel_reader in log.get_relations() for rel_reader in log.get_relations()
.map_err(|e| format!("get relations: {}", e))? { .map_err(|e| format!("get relations: {}", e))? {
let rel = read_relation(rel_reader)?; let rel = Relation::from_capnp(rel_reader)?;
let existing_version = by_uuid.get(&rel.uuid) let existing_version = by_uuid.get(&rel.uuid)
.map(|r| r.version) .map(|r| r.version)
.unwrap_or(0); .unwrap_or(0);
@ -677,7 +782,7 @@ impl Store {
let log = msg.init_root::<memory_capnp::node_log::Builder>(); let log = msg.init_root::<memory_capnp::node_log::Builder>();
let mut list = log.init_nodes(nodes.len() as u32); let mut list = log.init_nodes(nodes.len() as u32);
for (i, node) in nodes.iter().enumerate() { for (i, node) in nodes.iter().enumerate() {
write_content_node(list.reborrow().get(i as u32), node); node.to_capnp(list.reborrow().get(i as u32));
} }
} }
serialize::write_message(&mut writer, &msg) serialize::write_message(&mut writer, &msg)
@ -701,7 +806,7 @@ impl Store {
let log = msg.init_root::<memory_capnp::relation_log::Builder>(); let log = msg.init_root::<memory_capnp::relation_log::Builder>();
let mut list = log.init_relations(relations.len() as u32); let mut list = log.init_relations(relations.len() as u32);
for (i, rel) in relations.iter().enumerate() { for (i, rel) in relations.iter().enumerate() {
write_relation(list.reborrow().get(i as u32), rel); rel.to_capnp(list.reborrow().get(i as u32));
} }
} }
serialize::write_message(&mut writer, &msg) serialize::write_message(&mut writer, &msg)
@ -1834,135 +1939,7 @@ fn read_uuid(result: capnp::Result<&[u8]>) -> [u8; 16] {
out out
} }
fn read_content_node(r: memory_capnp::content_node::Reader) -> Result<Node, String> { // Serialization functions (read_content_node, write_content_node, read_relation,
Ok(Node { // write_relation, read_provenance) replaced by capnp_enum! + capnp_message!
uuid: read_uuid(r.get_uuid()), // macro invocations above. Node::from_capnp/to_capnp and Relation::from_capnp/to_capnp
version: r.get_version(), // are generated declaratively.
timestamp: r.get_timestamp(),
node_type: match r.get_node_type().map_err(|_| "bad node_type")? {
memory_capnp::NodeType::EpisodicSession => NodeType::EpisodicSession,
memory_capnp::NodeType::EpisodicDaily => NodeType::EpisodicDaily,
memory_capnp::NodeType::EpisodicWeekly => NodeType::EpisodicWeekly,
memory_capnp::NodeType::Semantic => NodeType::Semantic,
},
provenance: read_provenance(r.get_provenance().map_err(|_| "bad provenance")?)?,
key: read_text(r.get_key()),
content: read_text(r.get_content()),
weight: r.get_weight(),
category: match r.get_category().map_err(|_| "bad category")? {
memory_capnp::Category::General => Category::General,
memory_capnp::Category::Core => Category::Core,
memory_capnp::Category::Technical => Category::Technical,
memory_capnp::Category::Observation => Category::Observation,
memory_capnp::Category::Task => Category::Task,
},
emotion: r.get_emotion(),
deleted: r.get_deleted(),
source_ref: read_text(r.get_source_ref()),
created: read_text(r.get_created()),
retrievals: r.get_retrievals(),
uses: r.get_uses(),
wrongs: r.get_wrongs(),
state_tag: read_text(r.get_state_tag()),
last_replayed: r.get_last_replayed(),
spaced_repetition_interval: r.get_spaced_repetition_interval(),
position: r.get_position(),
community_id: None,
clustering_coefficient: None,
degree: None,
})
}
fn read_provenance(p: memory_capnp::Provenance) -> Result<Provenance, String> {
Ok(match p {
memory_capnp::Provenance::Manual => Provenance::Manual,
memory_capnp::Provenance::Journal => Provenance::Journal,
memory_capnp::Provenance::Agent => Provenance::Agent,
memory_capnp::Provenance::Dream => Provenance::Dream,
memory_capnp::Provenance::Derived => Provenance::Derived,
})
}
fn write_content_node(mut b: memory_capnp::content_node::Builder, node: &Node) {
b.set_uuid(&node.uuid);
b.set_version(node.version);
b.set_timestamp(node.timestamp);
b.set_node_type(match node.node_type {
NodeType::EpisodicSession => memory_capnp::NodeType::EpisodicSession,
NodeType::EpisodicDaily => memory_capnp::NodeType::EpisodicDaily,
NodeType::EpisodicWeekly => memory_capnp::NodeType::EpisodicWeekly,
NodeType::Semantic => memory_capnp::NodeType::Semantic,
});
b.set_provenance(match node.provenance {
Provenance::Manual => memory_capnp::Provenance::Manual,
Provenance::Journal => memory_capnp::Provenance::Journal,
Provenance::Agent => memory_capnp::Provenance::Agent,
Provenance::Dream => memory_capnp::Provenance::Dream,
Provenance::Derived => memory_capnp::Provenance::Derived,
});
b.set_key(&node.key);
b.set_content(&node.content);
b.set_weight(node.weight);
b.set_category(match node.category {
Category::General => memory_capnp::Category::General,
Category::Core => memory_capnp::Category::Core,
Category::Technical => memory_capnp::Category::Technical,
Category::Observation => memory_capnp::Category::Observation,
Category::Task => memory_capnp::Category::Task,
});
b.set_emotion(node.emotion);
b.set_deleted(node.deleted);
b.set_source_ref(&node.source_ref);
b.set_created(&node.created);
b.set_retrievals(node.retrievals);
b.set_uses(node.uses);
b.set_wrongs(node.wrongs);
b.set_state_tag(&node.state_tag);
b.set_last_replayed(node.last_replayed);
b.set_spaced_repetition_interval(node.spaced_repetition_interval);
b.set_position(node.position);
}
fn read_relation(r: memory_capnp::relation::Reader) -> Result<Relation, String> {
Ok(Relation {
uuid: read_uuid(r.get_uuid()),
version: r.get_version(),
timestamp: r.get_timestamp(),
source: read_uuid(r.get_source()),
target: read_uuid(r.get_target()),
rel_type: match r.get_rel_type().map_err(|_| "bad rel_type")? {
memory_capnp::RelationType::Link => RelationType::Link,
memory_capnp::RelationType::Causal => RelationType::Causal,
memory_capnp::RelationType::Auto => RelationType::Auto,
},
strength: r.get_strength(),
provenance: read_provenance(r.get_provenance().map_err(|_| "bad provenance")?)?,
deleted: r.get_deleted(),
source_key: read_text(r.get_source_key()),
target_key: read_text(r.get_target_key()),
})
}
fn write_relation(mut b: memory_capnp::relation::Builder, rel: &Relation) {
b.set_uuid(&rel.uuid);
b.set_version(rel.version);
b.set_timestamp(rel.timestamp);
b.set_source(&rel.source);
b.set_target(&rel.target);
b.set_rel_type(match rel.rel_type {
RelationType::Link => memory_capnp::RelationType::Link,
RelationType::Causal => memory_capnp::RelationType::Causal,
RelationType::Auto => memory_capnp::RelationType::Auto,
});
b.set_strength(rel.strength);
b.set_provenance(match rel.provenance {
Provenance::Manual => memory_capnp::Provenance::Manual,
Provenance::Journal => memory_capnp::Provenance::Journal,
Provenance::Agent => memory_capnp::Provenance::Agent,
Provenance::Dream => memory_capnp::Provenance::Dream,
Provenance::Derived => memory_capnp::Provenance::Derived,
});
b.set_deleted(rel.deleted);
b.set_source_key(&rel.source_key);
b.set_target_key(&rel.target_key);
}