Tree Sitter 的故事
Table of Contents
1. 树上的男爵
那时候在读卡尔维诺的小说集《我们的祖先》,译林出版社,吴正仪翻译。
Figure 1: Italo Calvino
《我们的祖先》是三部曲,包括
- 分成两半的子爵
- 树上的男爵
- 不存在的骑士
有人说这三个故事代表通向自由的三个阶段
- 摆脱不完整的人生
- 找到通向完整的道路
- 争取生存
我其实看不出来,只是单纯喜欢这种跳跃的语言。后来读作者晚年的《美国讲 稿》,卡尔维诺自己也说写的就是“轻”,“快”的感觉。
读卡尔维诺也是受王小波影响。王小波语言明快,是“初代程序员”,手搓汇编, 开发编辑器、输入法,是雷军的前辈,和求伯君平齐。
Figure 2: 王小波
2. 黑色的森林
而今一看到 Tree Sitter 就想到卡尔维诺《树上的男爵》。Tree Sitter 绑了 个软梯,而编辑器坐了上去。
Figure 3: Tree Sitter
小小编辑器,爬到树上干什么?或者用妈妈的话讲,地大容不下你,非得爬到树 上。《树上的男爵》的经验,那是为了追求完整的人生。不上树人生就不完整。 十二岁上树,一辈子待在树上,更不能人生完整。人世间有多少种树?人世间有 多少树,计算机就有多少语言。其中有 C 语言。Tree Sitter 这架软梯,就是用 C 语言写的。当我们谈论 Tree Sitter 的时候,我们在谈论什么?几种情况吧
tree-sitter命令行,在 CachyOS 下yay -S tree-sitter-cli安装tree-sitter init- 生成语法库框架tree-sitter generate- 生成代码tree-sitter build- 构建,前三个命令用于生成新语言库tree-sitter parse- 解析tree-sitter query- 查询tree-sitter highlight- 语法加亮,Github 基于此实现
tree-sitter的库,tree-sitter前面和后面可以加个语言,比如python-tree-sitter, 加在前面,表示可以在python语言里调用的tree-sitter库, 叫语言绑定 bindingpython-tree-sitter-python, 加在后面,表示解析 python 语言的tree-sitter插件库,由于还有前缀python,表示这个插件库插到python-tree-sitter里,叫语法 grammar。
支持的语法 Grammar 那就多了,世间多少树,计算机就有多少语言,Tree
Sitter 就有多少 Grammar 。如果没有,那就用 tree-sitter 造一个。
tree-sitter 支持的语言绑定 binding
- C/C++
- Go
- Node
- Java
- Python
- Rust
- Swift
- Zig
语言绑定与语法 Grammar 叉乘一下,可望见一片磅礴的黑森林。
3. 精神衣钵的枷锁
在这些语言里,有一门语言,具有最纯正的 C 语言血脉,甚至由 UNIX 之父亲 手打造,因此和 C 语言关系最差。
Figure 4: Ken Thompson
Go 继承了 C 语言精神衣钵,却与 C 语言运行时关系最差。Go 语言与 C 语言 的桥梁是 Cgo——Cgo 允许 Go 程序直接调用 C 代码,在以下情况使用:
- 需要重用已有的 C 库(如 libav, libcurl, openssl)
- 需要 Go 未暴露的底层系统调用
- C/C++ 遗留代码迁移到 Go
/*
#include <stdlib.h>
#include <string.h>
// Example C function
char* greet(const char* name) {
char* buffer = malloc(100);
snprintf(buffer, 100, "Hello %s", name);
return buffer;
}
*/
import "C"
import "unsafe"
func main() {
name := C.CString("Mehul")
defer C.free(unsafe.Pointer(name))
greeting := C.greet(name)
defer C.free(unsafe.Pointer(greeting))
fmt.Println(C.GoString(greeting))
}
tree-sitter 就是一个 C 语言的库,在 Go 语言中调用就叫 Go binding, 自然
使用 go-tree-sitter ^1
package main
import (
"fmt"
tree_sitter "github.com/tree-sitter/go-tree-sitter"
tsjs "github.com/tree-sitter/tree-sitter-javascript/bindings/go"
)
func main() {
code := []byte("const foo = 1 + 2")
parser := tree_sitter.NewParser()
defer parser.Close()
parser.SetLanguage(tree_sitter.NewLanguage(tsjs.Language()))
tree := parser.Parse(code, nil)
defer tree.Close()
root := tree.RootNode()
fmt.Println(root.ToSexp())
}
这样,你的 Go 程序就傍上 Cgo 了。你编译 Go 程序需要 C 编译器和运行时如影随形。
Figure 5: cgo and host C compiler
你跨平台的时候得问问 Cgo 同意不同意。Cgo 要是不同意你就跨不了,在 Linux 上 编译出所有架构的日子一去不返
# Build a single release binary, then gzip + sha256
define build-release
GOOS=$(word 1,$(subst /, ,$(1))) \
GOARCH=$(word 2,$(subst /, ,$(1))) \
CGO_ENABLED=0 \
go build -trimpath -ldflags="-s -w" -o $(RELEASE_DIR)/$(2) $(CMD_PATH)
$(if $(HAVE_UPX),upx --best --lzma $(RELEASE_DIR)/$(2),true)
gzip -fk $(RELEASE_DIR)/$(2)
cd $(RELEASE_DIR) && $(SHA256_CMD) $(2).gz > $(2).gz.sha256
endef
.PHONY: release
release:
@mkdir -p $(RELEASE_DIR)
$(call build-release,linux/amd64,$(BINARY)-linux-amd64)
$(call build-release,linux/arm64,$(BINARY)-linux-arm64)
$(call build-release,darwin/amd64,$(BINARY)-darwin-amd64)
$(call build-release,darwin/arm64,$(BINARY)-darwin-arm64)
$(call build-release,windows/amd64,$(BINARY)-windows-amd64.exe)
$(call build-release,windows/arm64,$(BINARY)-windows-arm64.exe)
@echo "---"
@echo "Release artifacts in $(RELEASE_DIR)/:"
@ls -lh $(RELEASE_DIR)/
4. 沉重的自由
Tree Sitter Cgo Free 是说如何在 Go 语言中使用 Tree Sitter ,同时又不触 碰 Cgo。
比如基于 Python Tree Sitter 的方案。在 Python 里调用 Python Tree Sitter,在 Go 里调用 Python,从输入传参,从输出得结果。
+-----+ +--------+ +---------------------+
| +------>| +--->| python-tree-sitter |
|dscli| |parse.py| | -{c, go, java, zig, |
|(Go) |<------+ |<---+ markdown, rust,...} |
+-----+ +--------+ +---------------------+
这方案可以用到 Tree Sitter 又不触碰 Cgo,但一次 Tree Sitter 解析需要几秒 时间,同时又依赖:
- python
- python-tree-sitter
- python-tree-sitter-{python, go, c, html, markdown, …}
其中 Tree Sitter Markdown 是一个复合 Tree Sitter,一般 Markdown 里有代 码块,代码块的解析又会用到代码语言的 Tree Sitter。这样去了 Cgo 这头牛 又牵来 Python 这头象,委实沉重。
5. Ccgo 上树计划
ccgo ^2 说摆脱 Cgo 的办法可以把 C 语言转译为 Go 语言。这是 ccgo
心目中的 C 现代化之路,有几个成功的案例
modernc.org/sqlite- Cgo free 的 Sqlitemodernc.org/tk9.0- CGo free 的跨平台 GUI 工具包
Figure 6: ccgo logo,把 C 吞到 Go 的肚子里
Go 中使用 Tree Sitter 还可以基于 ccgo 转译。 ccgo 把 C 语言转译为 Go
语言,结果也是 Cgo Free 的。
go install -v modernc.org/ccgo/v4@latest
ccgo 由 cznic(捷克无名氏)长期维护。代表项目 modernc.org/sqlite 就是
由 sqlite c 转为 Go ,性能、稳定性、完备性都是很好的。
用 ccgo 转译 tree-sitter 需要转译以下内容:
tree-sitter库(tree-sitterCLI 其实不需要转),tree-sitter库已 经转好了,- tree-sitter-{grammar},这就多了去了,我已经转的有
- tree-sitter-c
- tree-sitter-python
转换过程还算顺利,期间遇到几个 ccgo 小问题也已修复。捷克无名氏
(cznic)对外不发声音,对内代码的沟通极其详尽而顺畅。考虑 AI Editor
对 tree sitter 语言的要求基本是全要,算下来有将近 300 种,工程浩大。已转
的两个(C, Python)质量是可以的。
Tree Sitter Go 包设计
treesitter -- tree-sitter
├── clang -- tree-sitter-c
├── python -- tree-sitter-python
在这个设计下,每用到一种语言的 Tree Sitter,都要显式 import 。现在看这 不是一个好设计。因为代码加载解析是动态的,显式 import 很不方便。
6. 纯粹的刚烈
odvcencio 的 gotreesitter 让人惊艳
go get github.com/odvcencio/gotreesitter
github.com/odvcencio/gotreesitter 是一个纯 Go 的 Tree Sitter,在功能上
与 tree-sitter 库, tree-sitter-cli , 以及 tree-sitter-{lang,...} 几百
种语言库等价。迄今 gotreesitter 支持 206 种语言。其开发动机是在 Go 里实现 Cgo Free
- 完全消除 C 依赖,
- 其中 parser, lexer,query engine, incremental reparsing,arena allocator, 外部 scanner, tree cursor 等用 Go 重新实现,而不是转译,
- 唯一导入的是从 parser.c 抽取的 grammar blob,用工具 ts2go
这是一个雄心勃勃的项目,技术品味高,手段刚烈,简单留给用户
import (
"fmt"
"github.com/odvcencio/gotreesitter"
"github.com/odvcencio/gotreesitter/grammars"
)
func main() {
src := []byte(`package main
func main() {}
`)
lang := grammars.GoLanguage()
parser := gotreesitter.NewParser(lang)
tree, _ := parser.Parse(src)
fmt.Println(tree.RootNode())
}
查询引擎支持完整 S-expression pattern
q, _ := gotreesitter.NewQuery(
`(function_declaration name: (identifier) @fn)`, lang)
cursor := q.Exec(tree.RootNode(), lang, src)
for {
match, ok := cursor.NextMatch()
if !ok {
break
}
for _, cap := range match.Captures {
fmt.Println(cap.Node.Text(src))
}
}
支持编辑(replace, insert, delete)
rw := gotreesitter.NewRewriter(src)
rw.Replace(funcNameNode, []byte("newName"))
rw.InsertBefore(bodyNode, []byte("// added\n"))
rw.Delete(unusedNode)
newSrc, _ := rw.ApplyToTree(tree)
newTree, _ := parser.ParseIncremental(newSrc, tree)
7. 弹弓代码上树
GoTreeSitter 是我梦寐以求的,刚好满足我在 slingshot ^3规划的代码上树计划。
基于 GoTreeSitter 实现,实现过程中参考了 codebase-memory-mcp ^4
// GetNode 按字节位置获取最小节点。
func (ed *Editor) GetNode(uri string, pos uint32) (NodeInfo, error) {
doc, err := ed.getOrOpenDocument(uri)
if err != nil {
return NodeInfo{}, err
}
doc.Lock()
defer doc.Unlock()
ed.reloadIfExternalModified(doc)
if doc.tree == nil {
return NodeInfo{}, ErrDocumentNotReady
}
root := doc.tree.RootNode()
node := root.DescendantForByteRange(pos, pos)
if node == nil {
return NodeInfo{}, ErrNodeNotFound
}
return nodeToInfo(node, doc.language, doc.source), nil
}
对外暴露 MCP 服务(stdio)
// NewServer creates a new MCP server with the given store, analyzer, and options.
func NewServer(store *base.Store, analyzer *lsp.Analyzer, opts *Options) *Server {
ed := edit.NewEditor(opts.ProjectRoot)
return &Server{
store: store,
analyzer: analyzer,
ed: ed,
opts: opts,
}
}
数据库继续用 modernc.org/sqlite
_ "modernc.org/sqlite"
)
// Store provides SQLite-backed code graph storage.
// It manages projects, nodes, edges, ADRs, memos, and traces.
// All public methods are safe for concurrent use.
type Store struct {
mu sync.RWMutex
db *sql.DB
path string
}
// OpenStore opens (or creates) a SQLite database at the given path.
// The schema is automatically initialized on first use.
func OpenStore(dbPath string) (*Store, error) {
db, err := sql.Open("sqlite",
dbPath+"?_journal_mode=WAL&_cache_size=-65536")
if err != nil {
return nil, fmt.Errorf("open sqlite: %w", err)
}
// WAL mode + synchronous=NORMAL for better concurrency
pragmas := []string{
"PRAGMA journal_mode=WAL",
"PRAGMA synchronous=NORMAL",
"PRAGMA busy_timeout=5000",
"PRAGMA foreign_keys=ON",
配置到 dscli ^5
mcp-servers {
code {
name = code
type = local
command = slingshot
args = ["code", "serve"]
enabled = true
}
}
dscli tool list --category code , mcp 工具列表
名称分类描述 code_analysis code Analyze code complexity code_delete_project code Delete a project from th code_detect_changes code Detect code changes and code_edit_body code Replace the body of a de code_edit code Edit a file with write-t code_find_references code Find all references to a code_get_architecture code Get high-level architect code_get_code_snippet code Read source code for a f code_get_definitions code Get all definition tags code_get_graph_schema code Get the schema of the kn code_get_node code Get AST nodes from a fil code_get_project_root code Get the current project code_get_structure code Get the hierarchical cod code_get_text code Get source text from a f code_index_repository code Index a repository into code_index_status code Get the indexing status code_ingest_traces code Ingest runtime traces to code_list_projects code List all indexed project code_locate code Locate a symbol definiti code_manage_adr code Create or update Archite code_open_project code Switch to a different pr code_query_ast code Execute a tree-sitter S- code_query_graph code Execute a SQL query agai code_save_memo code Save a persistent memory code_search_code code Graph-augmented code sea code_search_graph code Search the code knowledg code_search_memos code Search persistent memori code_trace_path code Trace paths through the code_validate code Validate a file's syntax
那这个 slingshot code mcp 工具就用起来了。除了在 dscli 用,在其他 Code
Agent 原则上也可以用,因为标准 mcp 服务的缘故。