Skip to content

feat(trace): wire up --mode dispatch and ship tree-sitter by default - #260

Open
kryptt wants to merge 3 commits into
yoanbernabeu:mainfrom
kryptt:pr/treesitter-dispatch
Open

feat(trace): wire up --mode dispatch and ship tree-sitter by default#260
kryptt wants to merge 3 commits into
yoanbernabeu:mainfrom
kryptt:pr/treesitter-dispatch

Conversation

@kryptt

@kryptt kryptt commented May 29, 2026

Copy link
Copy Markdown

Problem

There are two related bugs in current main around the treesitter integration:

  1. --mode does nothing. cli/trace.go exposes --mode fast / --mode precise. The value is stored on TraceResult.Mode and surfaces in JSON output, but no code consults it to pick an extractor. Both cli/watch.go:1006 and cli/watch.go:2765 hardcode trace.NewRegexExtractor(); cli/trace.go reads the symbol store built by watch and never instantiates an extractor. The user-visible result: passing --mode precise produces the same symbols --mode fast would.

  2. Default users never get tree-sitter. Every trace/extractor_ts.go, every trace/extractor_ts_*_test.go, and the only call site through fsharp/ is gated behind //go:build treesitter. Stock go build and make build (no flags, no BUILD_TAGS) produce a binary that lacks the entire tree-sitter path. F# (PR feat(trace): add F# language support with Ionide tree-sitter grammar #152) and the rest of the precise-mode work landed but are dead code in any binary not built with the tag explicitly.

This PR fixes both at once.

Solution

1. Tree-sitter is always compiled in

Drop //go:build treesitter from trace/extractor_ts.go and its three test files. The grammars become unconditional dependencies via github.com/smacker/go-tree-sitter (already in go.sum) and the vendored fsharp/ package. Binary grows from ~23 MB to ~48 MB on x86_64-linux. Comparable to other code-search tools (ast-grep, rg).

2. New CompoundExtractor dispatches per-mode

`trace/compound.go` adds CompoundExtractor, which implements the existing SymbolExtractor interface by routing each file between TreeSitterExtractor and RegexExtractor:

```go
case ModeFast: // -> always RegexExtractor
case ModePrecise: // -> TreeSitterExtractor; error on unsupported extension
default: // ModeAuto: TS if grammar exists, else regex
```

`trace/languages.go` exposes HasTreeSitterGrammar(ext) bool and TreeSitterExtensions() []string as the single source of truth for which extensions are tree-sitter-backed in this build. Adding a new tree-sitter grammar in a future PR is one entry in that map plus the parser registration in NewTreeSitterExtractor — the dispatch picks it up automatically.

3. --mode auto is the new default

CLI flag default in cli/trace.go flips from \"fast\" to \"auto\". The same flip happens in config.DefaultConfig().Trace.Mode. The help text now describes all three modes. `trace.ParseMode` normalizes empty/whitespace input to `auto` and emits a one-line stderr warning for unknown values.

4. Watch flow uses the compound extractor

Both runWatch (single-project) and the workspace-mode runner now call trace.NewCompoundExtractor(trace.ParseMode(cfg.Trace.Mode)) instead of trace.NewRegexExtractor(). Function signatures that took `*trace.RegexExtractor` widen to `trace.SymbolExtractor` — the interface that already exists. No new dependency injection plumbing.

What the user sees

  • The same workspace, after upgrade, gets tree-sitter-quality symbols for .go, .js, .jsx, .ts, .tsx, .py, .php, .cs, .fs, .fsx, .fsi without changing config. For everything else, regex behavior is unchanged.
  • `grepai trace callers FOO` and friends finally respect `--mode precise`.
  • `--mode unknownvalue` warns once on stderr and continues with `auto` instead of failing.

Test plan

  • New `trace/compound_test.go` covers the dispatch matrix:

    • `ParseMode` normalization (empty / case / whitespace / unknown-falls-back)
    • `ModeAuto`: `.go` → TS, `.lua` → regex, `.xyz` → regex
    • `ModeFast`: every file → regex
    • `ModePrecise`: `.go` → TS, `.lua` → error with `--mode precise` and `.lua` in the message
    • End-to-end ExtractSymbols on a sample .go file producing the expected named symbols
    • Mode() returns the configured string for TraceResult.Mode display
    • SupportedLanguages() returns the sorted union of both underlying extractors
  • The existing `trace/extractor_ts_csharp_test.go`, `trace/extractor_ts_fsharp_test.go`, and `trace/extractor_ts_docstring_test.go` (previously build-tag-gated) now run unconditionally and continue to pass.

  • `go test ./...` is clean.

Things to think about during review

  • Binary size growth. ~23 MB → ~48 MB on x86_64-linux. If this is a problem we could add a make build-minimal target that re-introduces the build tag in the opposite direction (default = full; opt-out = small). Happy to do that in a follow-up if you want.
  • Cross-compilation. CGo is now in the default build path. The F# PR (feat(trace): add F# language support with Ionide tree-sitter grammar #152) already required this; CI presumably handles it. Worth confirming the release matrix isn't surprised.
  • No new languages here on purpose. This PR is intentionally restricted to plumbing so review can focus on dispatch correctness. A follow-up PR will use this foundation to add tree-sitter coverage for the union of (smacker-shipped grammars) ∩ (extensions already in indexer/scanner.SupportedExtensions), plus a vendored Wilfred tree-sitter-elisp for .el.

Closes nothing directly, but unblocks the latent intent of #152 and #176 (Lua fast-mode) by making the dispatch actually live.

kryptt added 3 commits May 29, 2026 09:33
Pre-existing bug: the `treesitter` build tag gated all tree-sitter code,
so stock `go build` produced a binary that could only do regex symbol
extraction. The `--mode fast`/`--mode precise` flag was a cosmetic
label on TraceResult.Mode that never actually selected an extractor —
both code paths called `trace.NewRegexExtractor()` regardless. F# (yoanbernabeu#152)
landed via tree-sitter but the dispatch to it was dead.

This change ships tree-sitter in the default build and makes the flag
mean what it says.

  * Drop `//go:build treesitter` from extractor_ts.go and its tests.
    `go build` now compiles grammars for Go, JS, JSX, TS, TSX, Python,
    PHP, C#, F# always. Binary grows from ~23 MB to ~48 MB.

  * Introduce `trace.CompoundExtractor`, which implements `SymbolExtractor`
    by routing per-file between the tree-sitter and regex extractors
    according to a Mode (auto / fast / precise). Tests in compound_test.go
    cover the dispatch matrix.

  * Add `trace.ParseMode` (normalizes user input; unknown values fall
    back to auto with a one-line warning to stderr) and `trace/languages.go`
    (`HasTreeSitterGrammar`, `TreeSitterExtensions` — single source of
    truth for which extensions have a compiled-in grammar).

  * `cli/watch.go` (both single-project and workspace runners) replaces
    `trace.NewRegexExtractor()` with `trace.NewCompoundExtractor(...)`.
    The relevant function signatures change `*trace.RegexExtractor` to
    `trace.SymbolExtractor` to widen to the interface.

  * `cli/trace.go` default for `--mode` flips from `fast` to `auto`; help
    text expanded to describe all three modes.

  * `config.TraceConfig.Mode` default flips from `fast` to `auto`.

No behavior change for the 9 already-supported languages at the symbol
level — they go from regex-quality to tree-sitter-quality, which is
strictly more accurate (e.g. Go method receivers, Python class
docstrings) without removing anything regex caught. Other extensions
keep their existing regex behavior.

Refs: extends yoanbernabeu#152 (F#) and yoanbernabeu#176 (Lua fast-mode) by making the dispatch
actually live.
Five small follow-ups from a self-review of the previous commit:

  * Consolidate the tree-sitter language registry. The extension list in
    languages.go and the extension→grammar map inside NewTreeSitterExtractor
    were two sources of truth that could drift. Merge into a single
    treeSitterLanguages map[string]func() *sitter.Language at the package
    level in languages.go. HasTreeSitterGrammar, TreeSitterExtensions, and
    NewTreeSitterExtractor all derive from it. Adding a new language is
    now one entry in that map plus the switch arm in walkNodeForSymbols.

  * Lazy tree-sitter init. NewCompoundExtractor no longer constructs the
    TreeSitterExtractor up front. ModeFast users (and ModeAuto users who
    only touch regex-only extensions) never pay the parser-allocation
    cost. Construction is deferred to the first file that needs it.

  * Drop the (always-nil) error from NewCompoundExtractor's return
    signature. The error claim was misleading; the only possible failure
    mode (lazy TS init) now surfaces from per-file route() calls.

  * Make ParseMode a pure normalizer. It now returns (Mode, ok bool)
    instead of writing the "unknown value" warning to os.Stderr itself.
    The CLI call sites in cli/watch.go log the warning where the I/O
    concern belongs and where the project name is in scope (helpful for
    workspace mode).

  * Library-friendly error message. The route() error no longer leaks
    the "--mode precise:" CLI flag name. Replaced with "trace: precise
    mode requires a tree-sitter grammar for extension %q (file %s); none
    compiled in" so downstream library consumers see a meaningful message.

Also: expanded compound_test.go with TestCompound_FastMode_NoTreeSitterAllocation,
TestCompound_AutoMode_LazyTreeSitter, TestHasTreeSitterGrammar_FromRegistry,
and TestTreeSitterExtensions_SortedAndDerived to lock in these invariants.
Test fixture switched from .pas to a more durable .lua/.rs pair.

No external behavior change. Same 48 MB binary, same dispatch behavior,
just less duplication and a smaller surface to maintain when PR 2 lands
the language additions.
A focused duplication-detection pass with `dupl -t 50` surfaced two
clone groups introduced by the previous commits. Both are now
collapsed:

  * cli/watch.go:1006-1010 vs cli/watch.go:2769-2773 — the two
    NewCompoundExtractor call sites were independently doing
    ParseMode → log warning on unknown value → NewCompoundExtractor.
    Factored into buildSymbolExtractor(rawMode, context), which keeps
    the warning helpful (mentions the project root or workspace
    project name + lists valid values) and reduces each call site to
    a one-liner. The fact that the workspace site now also lists the
    valid values in its warning is a small UX improvement that fell
    out of the unification.

  * trace/compound_test.go:170-175 vs 258-263 — two copies of the
    "is this slice strictly ascending?" check. Factored into a
    t.Helper-marked assertStrictlySorted(t, label, got). The strict
    inequality is the right contract for both call sites because
    both inputs come from set-then-sort and cannot contain duplicates.

Other dupl hits are either pre-existing in upstream code (the
RPG-init blocks in cli/watch.go, the per-language extractGoSymbol /
extractJSSymbol / etc. shapes in trace/extractor_ts.go) or
structurally forced by the SymbolExtractor interface (the route+
delegate body of ExtractSymbols / ExtractReferences / ExtractAll
on CompoundExtractor — different return types preclude a clean
helper without generics). Those stay as-is.

Verified: go vet ./... clean, go test ./... clean,
dupl -t 30 on the three new files reports only the structurally
forced interface-shaped clone in compound.go.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant