Skip to content

Commit dfd2b90

Browse files
authored
feat(extraction): index Metal (.metal) and CUDA (.cu/.cuh) via the C++ grammar (#147)
Ports the Metal and CUDA language support from upstream colby 1.2.0..1.4.1 (the two cheapest additions, since both dialects are close enough to C++ that the existing tree-sitter-cpp grammar extracts them). Both map to Language::Cpp — no new language variant, no new grammar crate, no new dependency, and the golden .schema is unchanged. - Metal (#1121): .metal maps to the C++ grammar. MSL puts attributes after the declarator (`float4 pos [[position]];`), a shape tree-sitter-cpp misparses into a spurious inheritance edge from the struct to a field's type. An offset-preserving pre-parse pass blanks `[[attribute]]` annotations, gated to .metal files only — in regular C++ the pre-declarator `[[nodiscard]]` position is legal syntax the grammar parses natively and is left byte-identical. - CUDA (#1172, language parts only — the namespace-qualified-name and template-argument-call work from the same upstream commit already shipped in v0.31.0 and is not re-ported here): .cu/.cuh map to the C++ grammar, and CUDA in a plain .h/.hpp is recognized by content. The host->kernel call edge now survives the `<<<grid, block>>>` launch syntax — an offset-preserving, brace-balanced pre-parse blank of the launch config leaves `kernel(args)` as an ordinary resolvable call (templated launches ride the existing template-arg stripping). Kernels defined through a name-in-first-argument macro (`DEFINE_FLASH_FORWARD_KERNEL(name, ...)`) are indexed under their real name, with the macro-name recovery tightly gated so gtest-style `TEST(...)` and ordinary macro calls are untouched. The `<<<>>>` and CUDA-construct blanks fire only for CUDA files/content, so non-CUDA C++ is byte-identical. New dedicated `metal` and `cuda` golden fixtures guard the behavior; the existing cpp/godot/ruby/mini goldens and the .schema are byte-identical (no regen). Local function-pointer launch dispatch is deferred.
1 parent 16c7f5b commit dfd2b90

22 files changed

Lines changed: 1179 additions & 15 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
__global__ void add_kernel(int* out, const int* a, const int* b) {
2+
int i = 0;
3+
out[i] = a[i] + b[i];
4+
}
5+
6+
template <typename T, int N>
7+
__global__ void scale_kernel(T* x, T factor) {
8+
x[0] = x[0] * factor;
9+
}
10+
11+
DEFINE_FLASH_FORWARD_KERNEL(my_kernel, int n) {
12+
int i = n;
13+
}
14+
15+
void launch(int* out, const int* a, const int* b, float* data) {
16+
add_kernel<<<grid, block>>>(out, a, b);
17+
scale_kernel<float, 256><<<grid, block>>>(data, 2.0f);
18+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
struct float4 {
2+
float x;
3+
float y;
4+
float z;
5+
float w;
6+
};
7+
8+
struct float2 {
9+
float u;
10+
float v;
11+
};
12+
13+
struct VertexIn {
14+
float4 position [[position]];
15+
float2 uv [[user(locn0)]];
16+
};
17+
18+
float4 tint(float4 color) {
19+
return color;
20+
}
21+
22+
vertex float4 vertex_main(VertexIn in [[stage_in]]) {
23+
return tint(in.position);
24+
}

‎crates/codegraph-bench/tests/equivalence.rs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,43 @@ fn cpp_db_is_self_equivalent_to_cpp_golden() {
7979
assert_equivalent(&cpp_db(), &cpp_golden_dir()).unwrap();
8080
}
8181

82+
#[test]
83+
fn generated_golden_matches_committed_metal_fixture() {
84+
// Guards Metal (#1121): `.metal`→cpp mapping + the `[[attribute]]` blank that
85+
// prevents the spurious `VertexIn extends float4` inheritance edge.
86+
let tempdir = TestDir::new("generated-golden-metal");
87+
write_golden(&metal_db(), tempdir.path()).unwrap();
88+
89+
let expected = load_golden(&metal_golden_dir()).unwrap();
90+
let actual = load_golden(tempdir.path()).unwrap();
91+
92+
diff_canonical(&expected, &actual, None).unwrap();
93+
}
94+
95+
#[test]
96+
fn metal_db_is_self_equivalent_to_metal_golden() {
97+
assert_equivalent(&metal_db(), &metal_golden_dir()).unwrap();
98+
}
99+
100+
#[test]
101+
fn generated_golden_matches_committed_cuda_fixture() {
102+
// Guards CUDA (#1172 CUDA-lang parts): `.cu`→cpp mapping, the `<<<…>>>`
103+
// launch-config blank that preserves the host→kernel Calls edge (plain +
104+
// templated), and macro-defined-kernel name recovery (`my_kernel`).
105+
let tempdir = TestDir::new("generated-golden-cuda");
106+
write_golden(&cuda_db(), tempdir.path()).unwrap();
107+
108+
let expected = load_golden(&cuda_golden_dir()).unwrap();
109+
let actual = load_golden(tempdir.path()).unwrap();
110+
111+
diff_canonical(&expected, &actual, None).unwrap();
112+
}
113+
114+
#[test]
115+
fn cuda_db_is_self_equivalent_to_cuda_golden() {
116+
assert_equivalent(&cuda_db(), &cuda_golden_dir()).unwrap();
117+
}
118+
82119
#[test]
83120
fn tier1_node_drift_is_reported() {
84121
let expected = load_golden(&mini_golden_dir()).unwrap();
@@ -157,6 +194,22 @@ fn cpp_golden_dir() -> PathBuf {
157194
workspace_root().join("reference/golden/cpp")
158195
}
159196

197+
fn metal_db() -> PathBuf {
198+
workspace_root().join("reference/golden/metal/colby.db")
199+
}
200+
201+
fn metal_golden_dir() -> PathBuf {
202+
workspace_root().join("reference/golden/metal")
203+
}
204+
205+
fn cuda_db() -> PathBuf {
206+
workspace_root().join("reference/golden/cuda/colby.db")
207+
}
208+
209+
fn cuda_golden_dir() -> PathBuf {
210+
workspace_root().join("reference/golden/cuda")
211+
}
212+
160213
struct TestDir {
161214
path: PathBuf,
162215
}

‎crates/codegraph-extract/src/engine.rs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ pub fn builtin_language_for_ext(ext: &str) -> Option<Language> {
5858
"java" => Language::Java,
5959
"c" | "h" => Language::C,
6060
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => Language::Cpp,
61+
// Metal Shading Language (≈ C++14) and CUDA (≈ C++ + dialect tokens) both
62+
// ride the C++ grammar with a dialect-specific pre-parse blank; no new
63+
// `Language` variant (upstream maps all three to `cpp`).
64+
"metal" | "cu" | "cuh" => Language::Cpp,
6165
"cs" => Language::CSharp,
6266
"php" | "module" | "install" | "theme" | "inc" => Language::Php,
6367
"rb" | "rake" => Language::Ruby,
@@ -203,7 +207,7 @@ pub fn extract_source(
203207
duration_ms: start.elapsed().as_millis() as i64,
204208
};
205209
}
206-
let parsed_source = spec.pre_parse(source);
210+
let parsed_source = spec.pre_parse(source, file_path);
207211
let Some(tree) = parser.parse(&parsed_source, None) else {
208212
return ExtractionResult {
209213
nodes: Vec::new(),
@@ -859,6 +863,14 @@ mod tests {
859863
assert_eq!(detect_language("src/lib.rs"), Language::Rust);
860864
}
861865

866+
#[test]
867+
fn metal_cu_cuh_map_to_cpp() {
868+
assert_eq!(detect_language("s.metal"), Language::Cpp);
869+
assert_eq!(detect_language("k.cu"), Language::Cpp);
870+
assert_eq!(detect_language("k.cuh"), Language::Cpp);
871+
assert_eq!(Language::ALL.len(), 36);
872+
}
873+
862874
#[test]
863875
fn extract_source_unknown_language_yields_empty_no_error() {
864876
let result = extract_source("mystery.unknownext", "content", None);

‎crates/codegraph-extract/src/lang/c.rs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ impl LanguageSpec for CSpec {
7979
fn extract_import(&self, node: Node<'_>, source: &str) -> Option<ImportInfo> {
8080
include_import(node, source)
8181
}
82+
fn pre_parse(&self, source: &str, _file_path: &str) -> String {
83+
if crate::lang::cpp::looks_like_cuda_source(source) {
84+
crate::lang::cpp::blank_cuda_constructs_str(source)
85+
} else {
86+
source.to_string()
87+
}
88+
}
8289
}
8390

8491
pub(crate) fn include_import(node: Node<'_>, source: &str) -> Option<ImportInfo> {
@@ -177,3 +184,21 @@ fn valid_ident(text: &str) -> bool {
177184
.is_some_and(|c| c == '_' || c.is_ascii_alphabetic())
178185
&& chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
179186
}
187+
188+
#[cfg(test)]
189+
mod tests {
190+
use super::*;
191+
192+
#[test]
193+
fn c_header_cuda_content_blanked() {
194+
let out = CSpec.pre_parse("__device__ int helper() { return 0; }", "h.h");
195+
assert!(!out.contains("__device__"));
196+
assert_eq!(out.len(), "__device__ int helper() { return 0; }".len());
197+
}
198+
199+
#[test]
200+
fn c_plain_untouched() {
201+
let src = "int add(int a, int b) { return a + b; }";
202+
assert_eq!(CSpec.pre_parse(src, "m.c"), src);
203+
}
204+
}

0 commit comments

Comments
 (0)