Contributor/agent notes. User-facing docs live in README.md; this is the
"how it's built and where the sharp edges are" companion.
A VGI worker (Rust, compiled binary) exposing
source-code structure to DuckDB/SQL over Arrow IPC. Built on the vgi crate
(crates.io), modeled on vgi-image / vgi-barcode. Catalog name code (single
main schema). Parsing via tree-sitter
plus a curated set of grammar crates.
Cargo.toml workspace; pins vgi = "0.9.0", tree-sitter + grammars
crates/code-worker/
src/main.rs Worker::new(); registers scalars + tables
src/parsing.rs PURE logic (no Arrow): language registry, parse,
symbols/imports/comments/strings/loc/queries + unit tests
src/arrow_io.rs VARCHAR cell reads + LIST(VARCHAR) builder + in-process scalar harness
src/scalar/{language,counts,lists,query,version,mod}.rs thin Arrow scalar adapters
src/table/{symbols,ts_nodes,supported_languages,mod}.rs thin Arrow table-producer adapters
tests/analyze.rs integration tests over `parsing` across rust/python/js/go
test/sql/*.test haybarn-unittest sqllogictest — authoritative E2E
Makefile test / test-unit / test-sql / lint / fmt / build / clean
Pattern: keep computation in parsing.rs (pure, unit-tested), keep Arrow
marshalling in arrow_io.rs + scalar/*.rs + table/*.rs (thin, harness-tested).
tree-sitter 0.25 is the parsing core. Each grammar crate exposes a
LANGUAGE: LanguageFn (or LANGUAGE_TYPESCRIPT for TS) constant; parsing.rs
maps the Lang enum to the grammar via .into().
Per-language tree-sitter queries drive every extraction:
symbols_querycaptures@<kind>.def(the whole definition node, for the line span) sibling-paired with@<kind>.name(the identifier).parsing::symbolspairs them and normalizes kinds to function/method/class/struct/enum/ interface/trait.imports_query/comments_query/strings_querycapture the relevant nodes; their text is returned as aVARCHAR[].ts_query/ts_nodesrun a user-supplied query — the power feature.
-
tree-sitter ABI / version pinning (the fiddly one). The grammar crates are at divergent versions (rust 0.24, python/js/go 0.25, ts/java/cpp 0.23, c/json 0.24), yet they all link against one
tree-sittercore. This works because every grammar — and the core — depends only on the ABI-stabletree-sitter-language ^0.1shim (resolved to0.1.7), which defines theLanguageFntype the grammars export. So the core version is chosen independently: we pintree-sitter = "0.25"(resolves to 0.25.10), whose MSRV (1.76) is under the workspacerust-version = 1.86. (0.26 also works; we stay on the well-worn 0.25 line.) Do NOT try to match grammar version numbers to the core — they intentionally diverge. -
tree-sitter0.25 query matches are astreaming-iterator.QueryCursor:: matches(...)returns aStreamingIterator, not a plainIterator. Iterate it withwhile let Some(m) = matches.next()(thestreaming_iterator::Streaming Iteratortrait is imported inparsing.rs). Hence the explicitstreaming-iteratordependency. -
haybarn-unittestskipsrequire vgi—.testfiles use explicitstatement ok+LOAD vgi;. Functions live under thecodecatalog, so each file doesSET search_path = 'code.main', thenUSE memorybeforeDETACH code. -
LIST(VARCHAR) return type must match between bind and process. The Arrow
DataType::Listpublished inon_bindmust exactly equal the array built inprocess(child field nameitem, nullable). Both go througharrow_io::list_varchar_type()/list_builder()so they cannot drift — same disciplinevgi-imageuses for its MAP/STRUCT returns. -
Scalars are positional-only; table functions take constants. The scalar
language/queryarguments are per-row VARCHAR columns (DuckDB constant- folds a literal), so one call can mix languages across rows. The table functions (symbols,ts_nodes) take bind-time constantsource/language/query, passed positionally — they read them viaconst_str(i)and validate the language/query aton_bindfor a clear early error. (The SDK registers these as positionalconst_args; DuckDB's binder rejects aname := valuecall form for them, so the.testsuite calls them positionally.) -
Robustness. Input is bounded (
MAX_SOURCE_BYTES = 16 MiB). tree-sitter recovers from any malformed source, so extraction is best-effort: garbage in → few/no symbols, empty lists, no panic. The only hard errors are an unknown language and a malformed user query — both caller mistakes, surfaced clearly. NULL in → NULL out / no rows.
cargo test --workspace # pure unit + arrow-boundary harness + integration
cargo clippy --all-targets --all-features -- -D warnings && cargo fmt --all -- --check
make test-sql # builds release, sets VGI_CODE_WORKER, haybarn over test/sql/*
make test # cargo test + sqlCI (.github/workflows/ci.yml) runs fmt/clippy/build/test plus a gated
e2e-sql job (installs uv + haybarn-unittest, runs make test-sql).
Scalars: language_of, count_lines, loc, count_functions,
extract_imports, extract_comments, extract_strings, ts_query,
code_version. Tables: symbols, ts_nodes, supported_languages. Garbage /
empty / oversized source → graceful empty / NULL / no rows; an unknown language
or malformed query is a clear error.