grove-cst 0.2.0

Core AST engine, grammar registry, fetch, and ingest for grove — the structural code-intelligence library behind the grove CLI and MCP server.
Documentation

grove-core

The structural code-intelligence library behind the grove CLI and MCP server. grove-core hosts the tree-sitter AST engine, the grammar registry, grammar fetch, and source ingest — the same engine the grove binary drives. It gives you structural, byte-precise, token-cheap access to a codebase: query definitions, sources, callers, and dependency maps without reading whole files.

Grammars load at runtime from a hosted WASM registry, so no grammar is compiled in and adding a language needs no recompile. The crate is clap-free — command-line concerns live in the grove binary, not here.

Install

Published on crates.io as grove-cst — CST for the concrete syntax trees tree-sitter builds (the plain grove-core name belongs to an unrelated crate). The library name is still grove_core, so alias it and your imports stay unchanged:

[dependencies]
grove_core = { package = "grove-cst", version = "0.1" }

Before the first crates.io release, depend on it by git instead:

[dependencies]
grove_core = { git = "https://github.com/Entelligentsia/grove", package = "grove-cst" }

Usage

The consumer-facing surface is the ops module — a small set of structural queries that work for any registered language. Before querying, provision the grammars for the target project once with init::provision_project: it detects the project's languages, fetches any missing grammar into the OS cache, and pins grove.lock. After that, every ops::* call resolves grammars from the cache.

use std::path::Path;
use grove_core::{init, ops};

fn main() -> anyhow::Result<()> {
    let project = Path::new(".");

    // 1. Provision grammars for the languages in this project. Fetches any
    //    missing grammar into the OS cache and pins grove.lock. Run once;
    //    pass `true` for a dry run (detect only, no network, no writes).
    for action in init::provision_project(project, false)? {
        println!("provisioned: {action}");
    }

    // 2. Query — grammars now resolve from the cache. Every definition under
    //    `src/`, gitignore-aware.
    for s in ops::symbols(&project.join("src"), None, None, false, false)? {
        println!("{} {}{}:{}", s.kind, s.name, s.file, s.line);
    }

    // 3. One symbol's full source, by name — no whole-file read.
    let hit = ops::source("src/lib.rs", Some("main"))?;
    println!("{}", hit.source);

    Ok(())
}

Offline / pinned registry: to skip the network and resolve grammars from a specific registry root instead, set GROVE_REGISTRY=<dir> (highest resolution precedence) and call the ops::* functions directly — provisioning is only needed to populate the cache.

Manual grammar management

provision_project is the batteries-included path (detect → fetch → lock). If you want direct control, the fetch and registry modules expose each step:

use std::path::Path;
use grove_core::{fetch, registry};

fn main() -> anyhow::Result<()> {
    // Discover what the hosted registry offers.
    for g in fetch::catalog_grammars()? {
        println!("{:<12} {:?}", g.name, g.extensions);
    }

    // Fetch specific grammars into the OS cache (pass `true` to re-download).
    let langs = vec!["rust".to_string(), "python".to_string()];
    fetch::run(&langs, false)?;

    // What's resolvable now (cache + any GROVE_REGISTRY / project registry)?
    println!("available: {}", registry::available().join(", "));
    println!("registry root: {}", registry::root().display());

    // Register (load + compile) one grammar by name — cached per process.
    let rust = registry::resolve("rust")?;
    println!("loaded {} v{}", rust.name, rust.version);

    // …or resolve the grammar for a given file by its extension.
    let g = registry::for_path(Path::new("src/lib.rs"))?;
    println!("src/lib.rs → {}", g.name);

    // Pin the resolved set into grove.lock (version + wasm sha256).
    let n = registry::write_lock_for(&langs, Path::new("grove.lock"))?;
    println!("locked {n} grammars");

    Ok(())
}

registry::resolve and for_path return a Grammar (the loaded wasm + compiled tags.scm + profile), cached per process. registry::search_path() shows the full resolution precedence, and registry::write_lock / locked_langs read and write grove.lock for reproducible pins.

The surface

Function Returns
ops::outline the definitions in one file (its symbol skeleton)
ops::symbols find symbols across a directory, gitignore-aware
ops::source the full source text of one symbol, by id or name
ops::check the syntactic defects (ERROR / MISSING) in one file
ops::callers every reference to a name, with its enclosing function
ops::map a directory's definitions and their outgoing references
ops::definition / ops::definition_at go-to-def by name or from a use site

Return types — Symbol, Defect, CallSite, FileMap, MapEntry, SourceResult — are re-exported at the crate root (e.g. grove_core::Symbol). init::provision_project is the grammar-provisioning entry point behind grove init. The lower-level engine, registry, fetch, and ingest modules are public for hosts that need deeper access.

Every result carries a stable symbol-id (<lang>:<relpath>#<name>@<line>, 1-based) you can pass between calls.

Grammars & the registry

grove-core resolves grammars from the first existing location (precedence): GROVE_REGISTRY env → <project>/.grove/grammars/ → the OS cache (~/.cache/grove/grammars on Linux) → a dev registry/ tree. Set GROVE_REGISTRY to point at a specific registry root for reproducible resolution. See the grove docs for the WASM registry, profiles, and the 27 supported languages.

Not an LSP

grove is a syntactic, tree-sitter-powered layer — it parses and locates, it does not do type inference, completion, rename, or type-resolved go-to-def. It is the cheap syntactic layer beneath where an LSP's semantics begin.

License

MIT © the grove authors. Part of the grove project.