Pure-Go tree-sitter parsers. Each grammar is compiled to WebAssembly with a pinned toolchain and translated to Go by wasm2go, then published as its own Go module: no cgo, no shared libraries, nothing fetched at runtime.
go get github.com/msuozzo/bonsai/bonsai-pythonimport bonsaipython "github.com/msuozzo/bonsai/bonsai-python"
src, _ := os.ReadFile("example.py")
p := bonsaipython.NewParser()
root, err := p.Parse(src)
if err != nil {
return err
}
for fn := range root.Find("function_definition") {
name := fn.ChildByField("name")
fmt.Printf("def %s @ line %d\n", name.Text(src), fn.StartPoint.Row+1)
}Import only the languages you need: every cost (download, compile,
binary size) scales with the modules you actually import. A Parser is
not goroutine-safe. Pool one per goroutine and reuse it across files.
Instantiation is the expensive part.
| module | grammar | pinned | module zip | binary cost¹ |
|---|---|---|---|---|
bonsai-bash |
tree-sitter-bash | v0.25.1 | 390 KB | +2.5 MB |
bonsai-dockerfile |
tree-sitter-dockerfile | v0.2.0 | 100 KB | +0.3 MB |
bonsai-go |
tree-sitter-go | v0.25.0 | 130 KB | +0.4 MB |
bonsai-gotemplate |
tree-sitter-go-template | master | 100 KB | +0.3 MB |
bonsai-groovy |
tree-sitter-groovy | initial | 250 KB | +1.6 MB |
bonsai-markdown |
tree-sitter-markdown (block + inline) | v0.5.3 | 430 KB | +2.2 MB |
bonsai-python |
tree-sitter-python | v0.25.0 | 160 KB | +0.6 MB |
bonsai-terraform |
tree-sitter-hcl (terraform dialect) | v1.2.0 | 110 KB | +0.3 MB |
bonsai-yaml |
tree-sitter-yaml | v0.7.2 | 130 KB | +0.5 MB |
¹ stripped-binary delta (-trimpath -ldflags='-s -w') over a 1.6 mb
baseline that imports no grammar module. the bonsai-markdown row covers
the combined block + inline parsers via NewFullParser, each individual
grammar accounting for roughly half.
Language modules encode the upstream grammar version in the tag's minor number and reserve the patch for re-releases (toolchain or runtime bumps at the same grammar):
bonsai-<lang>/v0.<encoded>.<respin>
encoded = major·10000 + minor·100 + patch (of the upstream grammar tag)
tree-sitter-python v0.25.0 → bonsai-python/v0.2500.0
…rebuilt with a newer toolchain → bonsai-python/v0.2500.1
tree-sitter-python v0.25.1 → bonsai-python/v0.2501.0
(Mirroring upstream tags verbatim was rejected: a re-release at the same
grammar version would have to squat on upstream's next patch number.)
Grammars without a semver upstream tag use encoded = 0 until upstream cuts a
release. The encoding exists for ordering, not parsing: the exact upstream pin
is always in the module's build.env and meta_gen.go header.
The root module (github.com/msuozzo/bonsai, the runtime) versions
independently as plain SemVer. Each language module pins the root version
it was generated against. A runtime-ABI change means regenerating and
re-releasing every language (CI enforces the regeneration half).
Releases are cut by tagging the root, replacing each language go.mod's
replace directive with a require of that tag, then tagging each
language module. No versions are tagged yet.
Each language module's *_gen.* files are produced hermetically in
Docker from the pins in bonsai-<lang>/build.env:
go generate ./bonsai-python # or: build/regen.sh python | allCI rebuilds every language in a parallel job matrix on each PR and fails if any generated byte differs from what's checked in.
Note: Regeneration builds
ncruces/wasm2gopinned by commit inDockerfile.builder: the pass that keeps large lexers within go/parser's nesting limits (ncruces/wasm2go#42) landed after the latest tagged release. The pin moves to a tagged release once one contains it.
The wasm→Go translation is mechanical: upstream authors retain copyright
over the code compiled into each module. Each generated module is a
derivative of the tree-sitter runtime and of its grammar (both MIT), so
their license texts ship inside the module (LICENSE.tree-sitter,
LICENSE.grammar) and are additionally embedded into consuming binaries
via licenses_gen.go (LicenseTreeSitter/LicenseGrammar), keeping the
required notices attached to compiled redistributions.