代码库图谱 · 用 tree-sitter 建一张符号索引
grok 用 tree-sitter 给五种语言建了一张作用域符号图(Bloop 风格),支撑跳转定义、查引用。这一章看它怎么解析、建什么节点与边、以及那个专为省内存设计的字符串驻留器。
代码库图谱 · 用 tree-sitter 建一张符号索引
xai-codebase-graph 是一张 Bloop 风格的作用域符号图:用 tree-sitter 解析代码,建出「符号在哪定义、被谁引用」的索引,支撑编辑器级的跳转定义 / 查引用。这一章看它怎么建。
五种语言,靠 tree-sitter 查询
它支持 Rust / TypeScript / JavaScript / Go / Python 五种语言。每种语言一个 TSLanguageConfig,核心是一段 tree-sitter S 表达式查询,约定捕获名:@name.definition.<kind>(定义)、@name.reference.<kind>(引用),外加别名对。<kind>(function/struct/const……)对应命名空间,用于「引用→定义」的命名空间感知匹配。
LanguageRegistry 有个 compute_query_hash()——对所有语言的查询字符串算哈希。查询一改,哈希变,缓存自动失效重建。把「规则」纳入缓存版本是个好习惯:否则你改了 query 却读到旧索引,查出来的结果对不上。
两种图:每文件 vs 跨文件
ScopeGraph(每文件):一张petgraph图,节点是Scope/Def/Import/Ref,边是RefToDef/DefToScope……。插入引用时从最紧作用域往根走(ScopeStack),按名字 + 命名空间匹配定义。这是经典的作用域图解析。ScopeGraphIndex(跨文件,真正的索引):扁平结构,definitions: 符号ID → [(路径ID, 行)]、references: ...、别名、文件元信息。这才是持久化、被查询的东西。
一个性能取舍:索引构建走快路径 extract_symbols_fast——跳过建图,直接返回定义/引用/别名列表(预计算好每个捕获是 def 还是 ref,避免热循环里反复比字符串,“快 2–3 倍”)。完整的 ScopeGraph 不持久化,需要时才重建。
字符串驻留器:为省内存而生
大仓库里符号名、路径重复度极高。StringInterner(interner.rs)是个内存优化的关键:一个字节 arena + offsets: Vec<(u32, u16)> + 用预哈希的 nohash_hasher 做查找,StringId(u32) 代替字符串到处传。定义/引用表里存的都是 StringId,行号用 u32(8 字节/条,而非 16)。7 月版源码的注释把目标写得很直白:把大仓库的索引从 ~1GB 压到 ~100MB(新代码删掉了这句注释,数据结构没变)。
构建流程:并行两阶段
IndexBuilder::build_fast:
- 收文件:先试
git2拿 tracked 文件(快),不行再用ignore并行 walk(尊重.gitignore),按支持的扩展名过滤。 - 阶段一(并行):
rayon线程池,par_chunks(100)逐文件process_file_fast——线程本地缓存 parser/query、5MB 大小闸、8KB 前缀查二进制、解析、抽符号。 - 阶段二(串行合并):归并进单个
ScopeGraphIndex(单一 interner)。按 5000 文件/批控制峰值内存。 compact()收缩。
持久化用自定义二进制格式(魔数 SGIX)——手写小端读写 interner arena、defs、refs、别名。识别不了魔数就当遗留、回退重建。
和《从 LLM 到 Coding Agent》对照
那本没有代码库图谱——它超出了「最小 agent」的范围。grok 这套(tree-sitter 多语言 + 作用域图 + 驻留器 + 并行构建 + 自定义序列化)是一个独立、完整的符号索引引擎。不过它有个出人意料的真相——它其实不喂给模型。下一章专门讲这件事。