You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ripwire does not index OCaml. On xapi-project/xen-api at b132418, ripwire 0.6.5 reports unindexed="ml:1052,mli:451,...". The 594 files it does index are the Python and shell around the edges, so --for, --callers and --impact never get to the toolstack itself.
Why it matters
xen-api is the XenServer/XCP-ng toolstack (xapi, xenopsd, networkd, rrdd), and it is mostly OCaml. Working on it, I keep running grep -rn across ocaml/ to find which module defines a function and who calls it, which is pretty much the question the map is there to answer. Right now my fallback is ctags. It finds top-level definitions, but not callers, and it skips let bindings local to a function.
STEP 0
I ran the parse-rate check from prompts/add-a-language.md with tree-sitter/tree-sitter-ocaml (MIT). It has separate grammars for .ml (ocaml) and .mli (ocaml_interface). Setup: py-tree-sitter 0.26.0, tree-sitter-ocaml 0.26.0, and a file counts as failed if its root node has_error.
Corpus
Commit
.ml clean
.mli clean
xapi-project/xen-api
b132418 (2026-10-02)
1052 / 1052
451 / 451
ocaml/dune
875dffd (2026-10-03)
1892 / 1893
936 / 936
The one failure in dune is test/blackbox-tests/test-cases/watching/multiple-errors-output.t/src/foo.ml, a fixture that is broken on purpose.
What I did not check:
whether ppx attributes and extension nodes ([@@deriving ...], [%...]) give useful tags.scm captures. They parse, I haven't looked further.
.mll (ocamllex) and .mly (menhir), which are separate languages.
call resolution through module paths, functors and open. I suspect Mod.f calls and first-class modules are where "refuse, don't guess" will have the most work to do, but you know that part of the code much better than I do.
So STEP 0 looks fine on these two corpora, at least. The next question is probably how module-qualified calls like Xapi_vdi.db_introduce bind, since most cross-file edges in an OCaml codebase go through them.
Thanks for the clear request and the concrete case, @gounthar — unindexed="ml:1052,mli:451" on xen-api says it plainly, and "which module defines this, and who calls it" is exactly the question ripwire is meant to answer.
OCaml isn't on our own near-term language list, but a community PR would be very welcome. New languages follow our language guide: data-first (a pinned, vendored tree-sitter grammar plus capture queries), shared mechanisms rather than per-language C++ unless a named hook is really needed, and red-first gates. If you or someone else wants to take it on, comment here and we'll point you at the right starting files.
@joyful-ii-V-I I'd like to take this on. Could you point me at the starting files you mentioned?
Before asking, I ran a local spike to see if it was worth it. Corpus is xen-api b132418, ripwire 36753d62, tree-sitter-ocaml v0.26.0 vendored, one Lang::OCaml with separate .ml and .mli query sets. A PR would pin tree-sitter-ocaml at 3b2e14e rather than the v0.26.0 tag, to pick up the fix for tree-sitter/tree-sitter-ocaml#153, which isn't released yet.
Definitions: 16 of 17 real lookups found. git grep gets 15, ctags 9. Xapi_vdi.update_allowed_operations gives its two definitions (.ml and .mli) instead of 18 grep hits.
Callers, when the qualifier is a file module: 19 of 19 right on Xapi_vdi.update_allowed_operations, where grep gives 179 hits.
Callers through functor-built or aliased modules are where it breaks. On a random sample of 98 such calls, 11 bound wrong or split. Nothing is keyed under a functor application, so the ref falls to the bare-name ladder, and Varstore_guard.start ends up bound to PV_Vnc.start.
Two questions before I go further:
If I read CONTRIBUTING right, the shared refusal for qualified calls whose qualifier keys nothing is slice 6a, on your side and not scheduled. Could it land together with OCaml call edges? I'd resolve module aliases (module T = Test_common) on the OCaml side first, so the refusal only drops what really can't be resolved. Refusing everything would also drop 41 alias calls from that sample that bind right today. If 6a can't come first, I'd open a definitions-only PR and leave call edges for later.
Scopes need an enclosing-scope walker. The one I wrote has the same shape as kotlinEnclosingScopeOf, only the node kinds differ (module_binding and module_name for OCaml, three owner kinds and type_identifier for Kotlin), so I'd rather make one table-driven walker for both than add a fourth per-language one. Does that make sense to you, or is there a reason they're kept apart that I'm not seeing?
@gounthar it's yours, thank you. That spike answers most of what we'd have asked, and the 16/17 definitions and 19/19 file-module callers on xen-api make a strong case on their own.
Your two questions
Ship definitions first, then call edges. Make the first PR definitions only: language, grammar, .ml/.mli captures and gates. It's reviewable on its own and gets xen-api's 1,500 files indexed. You can take slice 6a too, if you'd like. It's planned on our side but not scheduled, and it's a better fit landing with OCaml call edges than waiting. Keep it as its own PR (one slice per PR) and claim it in a new issue naming "6a". The refusal belongs in buildGraph (src/graph.h), where the canonical tier (appendCanonicalCandidates) finds no qualifier::name definition, so it applies to every language that sets Reference::qualifier, not to OCaml alone. Two things must stay byte-identical: Rust's keepRustQualifiedCandidates and C++'s keepStdQualifiedCandidates (a C++ qualifier can legitimately miss through an alias). Clojure's str/join is the known-gap case it should flip. test/langidentitycheck.sh gives you the evidence for both.
On module aliases (module T = Test_common), agreed: resolve them before refusing, or the refusal throws away the 41 good edges. Please don't build an OCaml-only alias table, though. Aliases are family 2 in the CONTRIBUTING map. Renames already reach the resolver as RawBind records (src/ingest_cache.h), so emit OCaml's module aliases into that same channel, ideally from a tags.scm capture. Then slice 2a (the shared binds channel) folds them in later without a rewrite. Functor applications (Varstore_guard.start) should be counted as unresolved, with a known-gap arm that names the slice that could fix them later. One more pointer: slice 6b (a language-neutral qualified-call resolver, claimed on Claim slice 6b: language-neutral qualified-call resolver, Elixir and Clojure #390) is where Mod.f resolution generalises across Elixir, Clojure and OCaml. A short note on Claim slice 6b: language-neutral qualified-call resolver, Elixir and Clojure #390 saying OCaml would be a data row there will save rework.
prompts/add-a-language.md: the step-by-step path. Read the "Adding a language: common stays common" section of CONTRIBUTING.md and its definition of done next to it.
src/ingest_crawl.h: kLangTable, one row per language. .ml and .mli are two grammars, so two rows with one Lang, or one row plus an extension switch. Follow how C++ maps several extensions.
queries/<lang>/tags.scm: use the shared capture vocabulary, with no OCaml-named capture kinds. Feat/clojure support #349 (Clojure) is the most recent language PR and the closest model.
Gates: test/langcheck.sh, plus a fixture row in test/langidentity/manifest.tsv so the identity harness covers OCaml. If extraction output moves, bump kParserVer; we set the final number at merge.
Ping us here when the definitions PR is up, or earlier if anything in the map doesn't match what you find in the code.
Activity
joyful-ii-V-I commentedon Oct 7, 2026
Thanks for the clear request and the concrete case, @gounthar —
unindexed="ml:1052,mli:451"on xen-api says it plainly, and "which module defines this, and who calls it" is exactly the question ripwire is meant to answer.OCaml isn't on our own near-term language list, but a community PR would be very welcome. New languages follow our language guide: data-first (a pinned, vendored tree-sitter grammar plus capture queries), shared mechanisms rather than per-language C++ unless a named hook is really needed, and red-first gates. If you or someone else wants to take it on, comment here and we'll point you at the right starting files.
gounthar commentedon Oct 15, 2026
@joyful-ii-V-I I'd like to take this on. Could you point me at the starting files you mentioned?
Before asking, I ran a local spike to see if it was worth it. Corpus is xen-api
b132418, ripwire36753d62, tree-sitter-ocaml v0.26.0 vendored, oneLang::OCamlwith separate.mland.mliquery sets. A PR would pin tree-sitter-ocaml at3b2e14erather than the v0.26.0 tag, to pick up the fix for tree-sitter/tree-sitter-ocaml#153, which isn't released yet.git grepgets 15, ctags 9.Xapi_vdi.update_allowed_operationsgives its two definitions (.mland.mli) instead of 18 grep hits.Xapi_vdi.update_allowed_operations, where grep gives 179 hits.Varstore_guard.startends up bound toPV_Vnc.start.Two questions before I go further:
module T = Test_common) on the OCaml side first, so the refusal only drops what really can't be resolved. Refusing everything would also drop 41 alias calls from that sample that bind right today. If 6a can't come first, I'd open a definitions-only PR and leave call edges for later.kotlinEnclosingScopeOf, only the node kinds differ (module_bindingandmodule_namefor OCaml, three owner kinds andtype_identifierfor Kotlin), so I'd rather make one table-driven walker for both than add a fourth per-language one. Does that make sense to you, or is there a reason they're kept apart that I'm not seeing?joyful-ii-V-I commentedon Oct 15, 2026
@gounthar it's yours, thank you. That spike answers most of what we'd have asked, and the 16/17 definitions and 19/19 file-module callers on xen-api make a strong case on their own.
Your two questions
Ship definitions first, then call edges. Make the first PR definitions only: language, grammar,
.ml/.mlicaptures and gates. It's reviewable on its own and gets xen-api's 1,500 files indexed. You can take slice 6a too, if you'd like. It's planned on our side but not scheduled, and it's a better fit landing with OCaml call edges than waiting. Keep it as its own PR (one slice per PR) and claim it in a new issue naming "6a". The refusal belongs inbuildGraph(src/graph.h), where the canonical tier (appendCanonicalCandidates) finds noqualifier::namedefinition, so it applies to every language that setsReference::qualifier, not to OCaml alone. Two things must stay byte-identical: Rust'skeepRustQualifiedCandidatesand C++'skeepStdQualifiedCandidates(a C++ qualifier can legitimately miss through an alias). Clojure'sstr/joinis the known-gap case it should flip.test/langidentitycheck.shgives you the evidence for both.On module aliases (
module T = Test_common), agreed: resolve them before refusing, or the refusal throws away the 41 good edges. Please don't build an OCaml-only alias table, though. Aliases are family 2 in the CONTRIBUTING map. Renames already reach the resolver asRawBindrecords (src/ingest_cache.h), so emit OCaml's module aliases into that same channel, ideally from atags.scmcapture. Then slice 2a (the shared binds channel) folds them in later without a rewrite. Functor applications (Varstore_guard.start) should be counted as unresolved, with a known-gap arm that names the slice that could fix them later. One more pointer: slice 6b (a language-neutral qualified-call resolver, claimed on Claim slice 6b: language-neutral qualified-call resolver, Elixir and Clojure #390) is whereMod.fresolution generalises across Elixir, Clojure and OCaml. A short note on Claim slice 6b: language-neutral qualified-call resolver, Elixir and Clojure #390 saying OCaml would be a data row there will save rework.Yes, one table-driven walker. That's slice 9a (One table-driven enclosing-scope and in-file test-scope walker instead of per-language copies #356, part 1). Read the comments above
rustEnclosingScopeOfandkotlinEnclosingScopeOf(src/ingest_names.h) first. Each says why it was kept apart, and if a table doesn't make the code smaller, a written finding is a valid result. Note that [help wanted] Go: a method's receiver type (and an interface's members) become the symbol's scope — slice 9a, Go row #403 (Go method scope) also asks for a Go row in the same table. Whoever lands the table first, the other adds a row; a comment on One table-driven enclosing-scope and in-file test-scope walker instead of per-language copies #356 and [help wanted] Go: a method's receiver type (and an interface's members) become the symbol's scope — slice 9a, Go row #403 keeps you two from colliding.Starting files
prompts/add-a-language.md: the step-by-step path. Read the "Adding a language: common stays common" section of CONTRIBUTING.md and its definition of done next to it.src/ingest_crawl.h:kLangTable, one row per language..mland.mliare two grammars, so two rows with oneLang, or one row plus an extension switch. Follow how C++ maps several extensions.third_party/deps/<lang>/: vendored grammars. Pinning3b2e14efor Stack overflow in scan_comment on deeply nested comments tree-sitter/tree-sitter-ocaml#153 is fine. Record the pin and the reason the way the other grammars do (third_party/patches/holds any local patch).queries/<lang>/tags.scm: use the shared capture vocabulary, with no OCaml-named capture kinds. Feat/clojure support #349 (Clojure) is the most recent language PR and the closest model.test/langcheck.sh, plus a fixture row intest/langidentity/manifest.tsvso the identity harness covers OCaml. If extraction output moves, bumpkParserVer; we set the final number at merge.Ping us here when the definitions PR is up, or earlier if anything in the map doesn't match what you find in the code.