greplm-core 0.6.0

Core indexing and search engine for greplm: a trigram code index for LLM agents.
Documentation
//! Ingest read path.
//!
//! Reading thousands of small source files is dominated by per-file syscall
//! overhead, not bandwidth, so the goal is high effective queue depth (many
//! reads in flight) rather than exotic zero-copy. The portable backend achieves
//! that with a rayon worker pool over buffered reads; the page cache keeps
//! re-reads (and the watch loop) cheap.
//!
//! This is a trait rather than a bare function because a batched-submission
//! backend (io_uring on Linux) is a plausible future addition. It is not worth
//! building yet: with a warm page cache, reads are ~5.6% of per-file indexing
//! CPU against ~92% for tree-sitter parsing, and reads already overlap parsing
//! across the worker pool. See docs/ROADMAP.md.

use std::path::Path;

use crate::config::Backend;
use crate::error::{Error, Result};

/// Abstraction over how file bytes are pulled in during indexing.
pub trait IoBackend: Send + Sync {
    /// Read the full contents of a file.
    fn read(&self, path: &Path) -> Result<Vec<u8>>;

    /// Human-readable backend name (for `status`).
    fn name(&self) -> &'static str;
}

/// Portable backend: a plain buffered read. Concurrency is supplied by the
/// caller running `read` from many rayon worker threads at once.
#[derive(Debug, Default, Clone, Copy)]
pub struct RayonBackend;

impl IoBackend for RayonBackend {
    fn read(&self, path: &Path) -> Result<Vec<u8>> {
        std::fs::read(path).map_err(|e| Error::io(path, e))
    }

    fn name(&self) -> &'static str {
        "rayon"
    }
}

/// Resolve the configured [`Backend`] to an implementation.
///
/// `config.backend` used to be parsed and then ignored — selection came from
/// build features alone — so setting it changed nothing. It is honored here, and
/// [`IoBackend::name`] (what `greplm status` prints) always names the backend
/// actually in use.
pub fn select(backend: Backend) -> Box<dyn IoBackend> {
    if backend == Backend::IoUringRemoved {
        tracing::warn!(
            "config.toml sets backend = \"io-uring\", which was removed in 0.5.0: it selected a \
             stub that did buffered reads anyway. Using the portable backend; set \
             backend = \"auto\" to silence this."
        );
    }
    match backend {
        Backend::Auto | Backend::Rayon | Backend::IoUringRemoved => Box::new(RayonBackend),
    }
}

/// Select a backend using the default configuration.
pub fn default_backend() -> Box<dyn IoBackend> {
    select(Backend::default())
}

#[cfg(test)]
mod tests {
    use super::*;

    /// `backend` in config.toml was parsed and then ignored: selection came from
    /// build features alone, so setting it changed nothing. This pins that it is
    /// honored, and that the reported name is the backend actually in use — which
    /// is what `greplm status` prints.
    #[test]
    fn config_backend_is_honored_and_reported() {
        assert_eq!(select(Backend::Rayon).name(), "rayon");
        assert_eq!(select(Backend::Auto).name(), "rayon");
    }

    /// A `config.toml` written before 0.5.0 may still say `backend = "io-uring"`.
    /// That must keep working — the value never selected anything real — and must
    /// report the backend actually in use, not the one requested.
    #[test]
    fn pre_0_5_io_uring_config_still_loads() {
        let cfg: crate::config::Config =
            toml::from_str("backend = \"io-uring\"").expect("legacy value must still parse");
        assert_eq!(cfg.backend, Backend::IoUringRemoved);
        assert_eq!(select(cfg.backend).name(), "rayon");
    }

    #[test]
    fn backend_reads_file_bytes_verbatim() {
        let dir = std::env::temp_dir().join(format!("greplm-io-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let f = dir.join("sample.txt");
        // Includes invalid UTF-8: the ingest path deals in bytes, not text.
        let body: &[u8] = b"line one\nline two\n\xff\xfe binary-ish\n";
        std::fs::write(&f, body).unwrap();
        for b in [Backend::Auto, Backend::Rayon] {
            assert_eq!(select(b).read(&f).unwrap(), body, "backend {b:?}");
        }
        assert!(select(Backend::Rayon).read(&dir.join("missing")).is_err());
        let _ = std::fs::remove_dir_all(&dir);
    }
}