Skip to content

Add C#, C++ and C - #7

Merged
lightningpixel merged 2 commits into
mainfrom
feat/c-family-and-csharp
Aug 9, 2026
Merged

Add C#, C++ and C#7
lightningpixel merged 2 commits into
mainfrom
feat/c-family-and-csharp

Conversation

@lightningpixel

Copy link
Copy Markdown
Owner

Builds on #6 (Java) — it is merged into this branch, so the diff includes it.

C++ and C share a grammar, a query and a resolution family. @vscode/tree-sitter-wasm ships no C grammar; the C++ one parses C without error and the tags a C file can produce are a subset. The family is shared because C++ calls into C without ceremony.

Two things the grammars forced

Inside a C++ class body the grammar names an inline member field_identifier, not identifier. The obvious pattern matched nothing, and the test caught it — expected undefined to be 'method*'. That node type occurs nowhere else, which makes the match a method by construction.

int Service::run() { … } is tagged a method directly, under run rather than Service::run. Containment cannot reach it from outside the class, and a receiver call only ever reaches a method, so tagging it a function would make it unreachable from obj.run().

Only definitions are tagged. A member declared in a class body is a field_declaration, not a function_definition, so a header declaration and its definition never become two symbols — the duplication problem simply does not arise.

C# writes each modifier as its own node rather than grouping them as Java does, and states member visibility, so it opts out of inheriting export from the enclosing type.

Measured on real projects

Project Language Files Calls resolved Refused as ambiguous
curl/curl C 1 068 42% 2%
JamesNK/Newtonsoft.Json C# 945 22% 57%
fmtlib/fmt C++ 77 21% 29%
google/gson Java 264 18% 32%

C gives the densest call graph of any language in the project — 21 229 edges from 10 427 symbols, with 2% ambiguity. Free functions with project-unique names are exactly what a syntactic resolver can follow.

C#, C++ and Java send most calls through a receiver on a typed object, and x.Get(…) cannot be attributed without the type of x. Refusing is the rule working; a wrong edge would be worse. C# is the weakest of the set, refusing more than half its call sites.

The README carries this table beside the language list rather than leaving it to be discovered.

Size

Packed 703 kB → 1.4 MB, unpacked 5.8 MB → 16.3 MB. The C# and C++ grammars are 4.9 and 5.1 MB against 405 kB for Java. If that is too much for a globally installed CLI, the alternative is fetching grammars on first use — but that trades away the "nothing to compile, no network" property the install section currently promises, so it is a decision rather than an optimisation.

Verification

  • npm run build, npx tsc --noEmit, npm test — 385 tests pass
  • npm run benchmark — ratio 0.699, success_delta 0
  • Eleven tests written first and watched fail with no language for …; the C++ inline-member one then failed a second time for a real reason, which is what uncovered field_identifier
  • Indexed curl, fmt and Newtonsoft.Json end to end
  • README figures re-measured after the change: 399 symbols, 1 177 edges, ~34,600 for the whole-project row

lightningpixel and others added 2 commits August 9, 2026 15:20
C++ and C share a grammar, a query and a resolution family.
@vscode/tree-sitter-wasm ships no C grammar; the C++ one parses C without
error, and the tags a C file can produce are a subset of the C++ ones. The
family is shared because C++ calls into C without ceremony.

Two things the grammars forced.

Inside a C++ class body the grammar names an inline member with a
field_identifier rather than an identifier, so the obvious pattern matched
nothing. That node type occurs nowhere else, which makes the match a method by
construction. Out-of-line `int Service::run()` is tagged a method too, under
`run` rather than `Service::run` — containment cannot reach it from outside
the class, and a receiver call only ever reaches a method.

Only definitions are tagged. A member declared in a class body is a
field_declaration, not a function_definition, so a header declaration and its
definition do not become two symbols.

C# writes each modifier as its own node rather than grouping them as Java
does, and states member visibility, so it opts out of inheriting export from
the enclosing type.

Measured on real projects rather than fixtures, and the results differ enough
to be worth publishing:

  curl            C     1068 files   42% resolved    2% ambiguous
  Newtonsoft.Json C#     945 files   22% resolved   57% ambiguous
  fmt             C++     77 files   21% resolved   29% ambiguous
  gson            Java    264 files  18% resolved   32% ambiguous

C is the densest call graph of any language here — free functions with
project-unique names are exactly what a syntactic resolver can follow. The
others send most calls through a receiver on a typed object, which it cannot.
The README carries the table rather than leaving it to be discovered.

Packed size 703 kB -> 1.4 MB; the C# and C++ grammars are 4.9 and 5.1 MB
unpacked against 405 kB for Java.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both found by indexing a project holding all ten languages at once and
counting the edges: eight, where ten were obvious. Java and C# were the two
missing.

A bare call reached only a non-method. That is right where a method needs its
receiver spelled out — ECMAScript, Python, Go, Rust — and wrong in Java, C#
and C++, where every function is a member and `helper()` inside a class means
`this.helper()`. The rule was dropping every intra-class call in three of the
languages just added. Languages now declare which convention they follow.

Relaxing it exposed a second defect. A constructor carries the name of the
type it builds, so with methods now reachable from a bare call, every
`new S()` matched both the class and its constructor and drew no edge at all —
gson lost 461 edges before this was fixed. Java and C# name constructors in
the grammar, so their queries no longer tag them; C++ has no constructor node,
so extract.ts drops a member that duplicates its type's name. Nothing is lost:
a constructor is not separately addressable, and its source sits inside the
class's range.

Re-measured on the same projects:

  gson            Java   4089 -> 5443 edges   18% -> 25% resolved
  Newtonsoft.Json C#     9293 -> 11701        22% -> 27%
  fmt             C++    3475 -> 4383         21% -> 27%
  curl            C     21229 -> 21239        42% -> 42%   (C has no methods)

The ten-language project now resolves every call it should: 10 edges, none
ambiguous, none unresolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lightningpixel
lightningpixel merged commit 2dea113 into main Aug 9, 2026
11 checks passed
@lightningpixel
lightningpixel deleted the feat/c-family-and-csharp branch August 9, 2026 16:05
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