Introduce lifecycle-aware Layer::Empty variant and update callers

The `Layer` enum is transformed into a lifecycle-aware state machine with an `Empty` variant representing an unconstructed directory. Read and query operations now explicitly panic when invoked on this state, enforcing explicit progression through `create()` before use. Iterator methods are updated to handle the new variant exhaustively, and module visibility constants are adjusted to support the refactored structure.
This commit is contained in:
Eric Coissac
2026-08-20 20:41:48 +02:00
parent 1c54e60c9a
commit c4b69e1af5
4 changed files with 311 additions and 130 deletions
+4
View File
@@ -183,6 +183,7 @@ impl SiblingLayerExt for obilayeredmap::Layer {
match self {
obilayeredmap::Layer::Count(l) => l.iter_siblings(annex),
obilayeredmap::Layer::Presence(l) => l.iter_siblings(annex),
obilayeredmap::Layer::Empty { .. } => panic!("iter_siblings() called on an Empty layer"),
}
}
@@ -190,6 +191,7 @@ impl SiblingLayerExt for obilayeredmap::Layer {
match self {
obilayeredmap::Layer::Count(l) => l.iter_siblings_batch(annex, batch_size),
obilayeredmap::Layer::Presence(l) => l.iter_siblings_batch(annex, batch_size),
obilayeredmap::Layer::Empty { .. } => panic!("iter_siblings_batch() called on an Empty layer"),
}
}
@@ -197,6 +199,7 @@ impl SiblingLayerExt for obilayeredmap::Layer {
match self {
obilayeredmap::Layer::Count(l) => l.iter_minorants(annex),
obilayeredmap::Layer::Presence(l) => l.iter_minorants(annex),
obilayeredmap::Layer::Empty { .. } => panic!("iter_minorants() called on an Empty layer"),
}
}
@@ -204,6 +207,7 @@ impl SiblingLayerExt for obilayeredmap::Layer {
match self {
obilayeredmap::Layer::Count(l) => l.iter_minorants_batch(annex, batch_size),
obilayeredmap::Layer::Presence(l) => l.iter_minorants_batch(annex, batch_size),
obilayeredmap::Layer::Empty { .. } => panic!("iter_minorants_batch() called on an Empty layer"),
}
}
}
+66 -5
View File
@@ -12,25 +12,50 @@
//! reimplement locally, minus its one sibling-specific method
//! (`iter_minorants_batch`, which stays an extension trait over there —
//! `obilayeredmap` has no business knowing about sibling annexes).
//!
//! `Layer` is meant to eventually represent a layer's *whole* life —
//! empty shell, under construction, ready to read — not just the
//! ready-to-read state, so the same type follows a layer from creation
//! through querying instead of construction living as disconnected free
//! functions elsewhere that happen to write into the same directory (see
//! `DevDocMD/implementation/partition_layer_cache.md`). [`Layer::Empty`]
//! is the first step: a directory and nothing else, able to hand out the
//! paths a builder needs, not yet able to build anything itself.
use std::path::Path;
use std::path::{Path, PathBuf};
use obicompactvec::{PersistentBitMatrix, PersistentCompactIntMatrix};
use obikseq::CanonicalKmer;
use crate::error::OLMResult;
use crate::layer::{LayerContent, TypedLayer, COUNTS_DIR};
use crate::layer::{LayerContent, TypedLayer, COUNTS_DIR, PRESENCE_DIR};
use crate::meta::IndexMode;
use crate::mphf_layer::EvidenceKind;
use crate::mphf_layer::{EvidenceKind, EVIDENCE_FILE, FINGERPRINT_FILE, MPHF_FILE, UNITIGS_FILE};
/// One layer, its content (`Count`/`Presence`) resolved at open time rather
/// than at compile time — see the module docs.
/// One layer, at any point in its life — see the module docs. Only
/// [`Empty`](Layer::Empty) and the two ready-to-read states
/// (`Count`/`Presence`) exist so far; states in between (unitigs written,
/// MPHF built, evidence built, matrix not yet built) are not represented
/// yet.
pub enum Layer {
/// Just a directory — nothing built yet. Every method below other than
/// the path accessors panics on this variant: calling them means the
/// caller assumed a layer was ready when it wasn't, an implementation
/// error to surface loudly, not paper over with a default value.
Empty { dir: PathBuf },
Count(TypedLayer<PersistentCompactIntMatrix>),
Presence(TypedLayer<PersistentBitMatrix>),
}
impl Layer {
/// An empty shell at `dir` — creates the directory (if it doesn't
/// already exist) but nothing inside it. The starting state for
/// building a new layer.
pub fn create(dir: &Path) -> std::io::Result<Self> {
std::fs::create_dir_all(dir)?;
Ok(Layer::Empty { dir: dir.to_owned() })
}
/// Open one layer, auto-detecting count vs. presence from what is
/// actually on disk (`counts/` present and wanted, else presence) — the
/// single source of truth every caller that opens a layer's own matrix
@@ -46,10 +71,40 @@ impl Layer {
TypedLayer::<PersistentBitMatrix>::open(layer_dir, mode).map(Layer::Presence)
}
// ── Paths — only meaningful before anything is built ───────────────
//
// Once a layer is `Count`/`Presence`, its caller already knows the
// directory (it had to pass it to `open`) — these exist for builder
// code holding an `Empty` layer, so the `mphf.bin`/`unitigs.bin`/…
// naming stays defined once, here, rather than re-declared as
// string literals at every call site that writes into a layer
// directory (the same duplication `layer_dir`/`index_dir` fixed one
// level up — see `DevDocMD/implementation/partition_layer_cache.md`).
/// This layer's own directory. Panics on `Count`/`Presence` — by that
/// point the caller already has the directory it opened with; asking
/// again here would mean it lost track of its own state.
pub fn dir(&self) -> &Path {
match self {
Layer::Empty { dir } => dir,
_ => panic!("Layer::dir() only available on Empty — caller already has this path"),
}
}
pub fn mphf_path(&self) -> PathBuf { self.dir().join(MPHF_FILE) }
pub fn unitigs_path(&self) -> PathBuf { self.dir().join(UNITIGS_FILE) }
pub fn evidence_path(&self) -> PathBuf { self.dir().join(EVIDENCE_FILE) }
pub fn fingerprint_path(&self) -> PathBuf { self.dir().join(FINGERPRINT_FILE) }
pub fn counts_dir(&self) -> PathBuf { self.dir().join(COUNTS_DIR) }
pub fn presence_dir(&self) -> PathBuf { self.dir().join(PRESENCE_DIR) }
// ── Ready-only surface ───────────────────────────────────────────────
pub fn content(&self) -> LayerContent {
match self {
Layer::Count(_) => LayerContent::Count,
Layer::Presence(_) => LayerContent::Presence,
Layer::Empty { .. } => panic!("Layer::content() called on an Empty layer"),
}
}
@@ -57,6 +112,7 @@ impl Layer {
match self {
Layer::Count(l) => l.evidence_kind(),
Layer::Presence(l) => l.evidence_kind(),
Layer::Empty { .. } => panic!("Layer::evidence_kind() called on an Empty layer"),
}
}
@@ -64,6 +120,7 @@ impl Layer {
match self {
Layer::Count(l) => l.n(),
Layer::Presence(l) => l.n(),
Layer::Empty { .. } => panic!("Layer::n() called on an Empty layer"),
}
}
@@ -71,6 +128,7 @@ impl Layer {
match self {
Layer::Count(l) => l.find_slot(kmer),
Layer::Presence(l) => l.find_slot(kmer),
Layer::Empty { .. } => panic!("Layer::find_slot() called on an Empty layer"),
}
}
@@ -81,6 +139,7 @@ impl Layer {
match self {
Layer::Count(l) => l.index_batch(kmers),
Layer::Presence(l) => l.index_batch(kmers),
Layer::Empty { .. } => panic!("Layer::index_batch() called on an Empty layer"),
}
}
@@ -88,6 +147,7 @@ impl Layer {
match self {
Layer::Count(l) => l.n_cols(),
Layer::Presence(l) => l.n_cols(),
Layer::Empty { .. } => panic!("Layer::n_cols() called on an Empty layer"),
}
}
@@ -108,6 +168,7 @@ impl Layer {
o.extend(c.iter().map(|&v| v != 0));
}
}
Layer::Empty { .. } => panic!("Layer::fill_sub_matrix_carries() called on an Empty layer"),
}
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ use crate::mphf_layer::MphfLayer;
pub(crate) use crate::mphf_layer::UNITIGS_FILE;
pub(crate) const COUNTS_DIR: &str = "counts";
const PRESENCE_DIR: &str = "presence";
pub(crate) const PRESENCE_DIR: &str = "presence";
// ── Trait ─────────────────────────────────────────────────────────────────────