Add Wolfram Language support (64 languages) + fix caller attribution

- Vendor LumaKernel/tree-sitter-wolfram grammar (ABI 13, parser.c + scanner.c)
- Register Wolfram language: .wl/.wls extensions, set_delayed_top/set_top/
  set_delayed/set as function types, apply as call type, get_top + Needs[]
  as import types
- Add CBM_LANG_WOLFRAM enum, lang spec entry, and grammar bridge
- Extract defs: resolve function name from apply(user_symbol("f"), ...) LHS
  of set/set_delayed definitions; recurse into function bodies for nested defs
- Extract calls: skip apply nodes that are definition LHS (not a call)
- Extract imports: handle << "file.wl" (get_top) and Needs["Package`"]
- Fix caller attribution: add func_kinds_wolfram to func_kinds_for_lang and
  add Wolfram name extraction in compute_func_qn (extract_unified.c) so the
  scope stack correctly tracks enclosing function for all call sites
- Add MATLAB function_call + command call extraction (63 -> 64 with Wolfram)
- Add Lean 4 apply call extraction with type-position filter
- Add regression tests for Wolfram, MATLAB, Lean calls + FORM pipeline test
This commit is contained in:
Martin Vogel committed 2026-03-07 18:01:27 +01:00
1 parent ced21194ac
commit 497dd6e61a
21 files changed
+115082 -14

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# Vendored tree-sitter grammars — machine-generated, hide from stats and diffs
internal/cbm/vendored/grammars/**/parser.c linguist-generated=true diff=false
internal/cbm/vendored/grammars/**/scanner.c linguist-generated=true diff=false
+3 -1
View File
@@ -128,6 +128,7 @@ func Shutdown() {
C.cbm_shutdown()
}
// ProfileStats holds accumulated profiling data from C extraction.
type ProfileStats struct {
ParseNs uint64
@@ -209,7 +210,8 @@ var languageToC = map[lang.Language]C.CBMLanguage{
lang.MATLAB: C.CBM_LANG_MATLAB,
lang.Lean: C.CBM_LANG_LEAN,
lang.FORM: C.CBM_LANG_FORM,
lang.Magma: C.CBM_LANG_MAGMA,
lang.Magma: C.CBM_LANG_MAGMA,
lang.Wolfram: C.CBM_LANG_WOLFRAM,
}
// ParseTimeoutMicros is the default per-file parse timeout (10 seconds).
+2
View File
@@ -74,6 +74,7 @@ typedef enum {
CBM_LANG_LEAN,
CBM_LANG_FORM,
CBM_LANG_MAGMA,
CBM_LANG_WOLFRAM,
CBM_LANG_COUNT
} CBMLanguage;
@@ -295,6 +296,7 @@ void cbm_shutdown(void);
void cbm_get_profile(uint64_t* parse_ns, uint64_t* extract_ns, uint64_t* files);
void cbm_reset_profile(void);
// --- Internal helpers used by extractors ---
// Growable array push functions (arena-allocated, no individual free needed).
+70
View File
@@ -10,8 +10,49 @@ static void walk_calls(CBMExtractCtx* ctx, TSNode node, const CBMLangSpec* spec)
static char* extract_callee_name(CBMArena* a, TSNode node, const char* source, CBMLanguage lang);
static void extract_jsx_refs(CBMExtractCtx* ctx, TSNode node);
// Lean 4: check if an apply node is inside a type annotation.
// Strategy: walk up to the nearest declaration boundary; if the apply falls
// inside that declaration's explicit_binder/implicit_binder, or before the
// body field, it's a type annotation. We check byte ranges: a call is valid
// only if it overlaps the body range of the enclosing declaration.
static bool lean_is_in_type_position(TSNode node) {
TSNode cur = ts_node_parent(node);
for (int depth = 0; depth < 20; depth++) {
if (ts_node_is_null(cur)) return false;
const char* pk = ts_node_type(cur);
// Inside a binder — definitely type position
if (strcmp(pk, "explicit_binder") == 0 ||
strcmp(pk, "implicit_binder") == 0 ||
strcmp(pk, "instance_binder") == 0) return true;
// At a declaration boundary: check if apply is inside the body field
if (strcmp(pk, "def") == 0 || strcmp(pk, "theorem") == 0 ||
strcmp(pk, "instance") == 0 || strcmp(pk, "abbrev") == 0 ||
strcmp(pk, "structure") == 0 || strcmp(pk, "inductive") == 0) {
// Check if apply comes after the type annotation.
// Strategy: if the node starts after the end of the "type" field, it's in value position.
// If there's no "type" field, allow the call (no annotation to filter).
TSNode type_field = ts_node_child_by_field_name(cur, "type", 4);
if (ts_node_is_null(type_field)) return false; // no type annotation → allow call
uint32_t type_end = ts_node_end_byte(type_field);
uint32_t node_start = ts_node_start_byte(node);
// If apply starts after the type annotation ends, it's a value (call)
if (node_start > type_end) return false;
return true; // apply is within or before type annotation → type position
}
cur = ts_node_parent(cur);
}
return false;
}
// Extract callee name from a call node
static char* extract_callee_name(CBMArena* a, TSNode node, const char* source, CBMLanguage lang) {
// Lean 4: apply — name field is callee. Skip if in a type annotation position.
// Must be checked before the generic "name" field handler below.
if (lang == CBM_LANG_LEAN && strcmp(ts_node_type(node), "apply") == 0) {
if (lean_is_in_type_position(node)) return NULL;
// Fall through to generic handler
}
// Try "function" field (most languages: call_expression, etc.)
TSNode func_node = ts_node_child_by_field_name(node, "function", 8);
if (!ts_node_is_null(func_node)) {
@@ -138,6 +179,35 @@ static char* extract_callee_name(CBMArena* a, TSNode node, const char* source, C
}
}
// MATLAB: command node — first child is command_name (not identifier)
if (lang == CBM_LANG_MATLAB && strcmp(ts_node_type(node), "command") == 0) {
if (ts_node_child_count(node) > 0) {
return cbm_node_text(a, ts_node_child(node, 0), source);
}
}
// Wolfram: apply — first named child is callee (user_symbol or builtin_symbol)
// Skip if this apply is the LHS of a set/set_delayed definition (top or nested)
if (lang == CBM_LANG_WOLFRAM && strcmp(ts_node_type(node), "apply") == 0) {
TSNode parent = ts_node_parent(node);
if (!ts_node_is_null(parent)) {
const char* pk = ts_node_type(parent);
if ((strcmp(pk, "set_delayed_top") == 0 || strcmp(pk, "set_top") == 0 ||
strcmp(pk, "set_delayed") == 0 || strcmp(pk, "set") == 0) &&
ts_node_named_child_count(parent) > 0 &&
ts_node_eq(ts_node_named_child(parent, 0), node)) {
return NULL;
}
}
if (ts_node_named_child_count(node) > 0) {
TSNode head = ts_node_named_child(node, 0);
const char* hk = ts_node_type(head);
if (strcmp(hk, "user_symbol") == 0 || strcmp(hk, "builtin_symbol") == 0)
return cbm_node_text(a, head, source);
}
return NULL;
}
// Generic fallback: first child
if (ts_node_child_count(node) > 0) {
TSNode first = ts_node_child(node, 0);
+19 -1
View File
@@ -237,6 +237,21 @@ static TSNode resolve_func_name(TSNode node, CBMLanguage lang, const char* sourc
// Magma: function/procedure/intrinsic_definition — name via "name" field (standard)
// Both use standard "name" field which is handled above at line 33
// Wolfram: set_delayed_top/set_top — LHS is apply(user_symbol("f"), ...) for f[x_] := ...
if (lang == CBM_LANG_WOLFRAM &&
(strcmp(kind, "set_delayed_top") == 0 || strcmp(kind, "set_top") == 0 ||
strcmp(kind, "set_delayed") == 0 || strcmp(kind, "set") == 0)) {
if (ts_node_named_child_count(node) > 0) {
TSNode lhs = ts_node_named_child(node, 0);
if (strcmp(ts_node_type(lhs), "apply") == 0 && ts_node_named_child_count(lhs) > 0) {
TSNode head = ts_node_named_child(lhs, 0);
if (strcmp(ts_node_type(head), "user_symbol") == 0) return head;
}
}
TSNode null_node = {0};
return null_node;
}
// C/C++/CUDA/GLSL: function_definition — name is inside the declarator chain
// C grammar: function_definition{declarator:function_declarator{declarator:identifier}}
if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
@@ -1897,7 +1912,10 @@ static void walk_defs(CBMExtractCtx* ctx, TSNode node, const CBMLangSpec* spec)
// Function types
if (cbm_kind_in_set(node, spec->function_node_types)) {
extract_func_def(ctx, node, spec);
return; // don't recurse into function bodies for nested defs
// Wolfram: continue recursing — nested set_delayed inside Module/Block are valid defs
if (ctx->language != CBM_LANG_WOLFRAM) {
return; // don't recurse into function bodies for nested defs
}
}
// Rust impl blocks
+59
View File
@@ -14,6 +14,7 @@ static void parse_c_imports(CBMExtractCtx* ctx);
static void parse_ruby_imports(CBMExtractCtx* ctx);
static void parse_lua_imports(CBMExtractCtx* ctx);
static void parse_generic_imports(CBMExtractCtx* ctx, const char* node_type);
static void parse_wolfram_imports(CBMExtractCtx* ctx);
// Helper: strip quotes from a string literal
static char* strip_quotes(CBMArena* a, const char* s) {
@@ -530,6 +531,61 @@ static void parse_generic_imports(CBMExtractCtx* ctx, const char* node_type) {
}
}
// --- Wolfram imports ---
// get_top: << "package" (Get["file"])
// apply where first child is builtin_symbol "Needs" with string arg
static void walk_wolfram_imports(CBMExtractCtx* ctx, TSNode node) {
CBMArena* a = ctx->arena;
const char* kind = ts_node_type(node);
// get_top: << "path" → import
if (strcmp(kind, "get_top") == 0) {
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_named_child(node, i);
const char* ck = ts_node_type(child);
if (strcmp(ck, "string") == 0 || strcmp(ck, "user_symbol") == 0) {
char* text = cbm_node_text(a, child, ctx->source);
if (text && text[0]) {
char* path = strip_quotes(a, text);
CBMImport imp = {.local_name = path_last(a, path), .module_path = path};
cbm_imports_push(&ctx->result->imports, a, imp);
}
break;
}
}
}
// Needs["package`"] — apply where head is builtin_symbol "Needs"
if (strcmp(kind, "apply") == 0 && ts_node_named_child_count(node) >= 2) {
TSNode head = ts_node_named_child(node, 0);
if (strcmp(ts_node_type(head), "builtin_symbol") == 0) {
char* name = cbm_node_text(a, head, ctx->source);
if (name && strcmp(name, "Needs") == 0) {
// Second named child should be the string argument
TSNode arg = ts_node_named_child(node, 1);
char* text = cbm_node_text(a, arg, ctx->source);
if (text && text[0]) {
char* path = strip_quotes(a, text);
CBMImport imp = {.local_name = path_last(a, path), .module_path = path};
cbm_imports_push(&ctx->result->imports, a, imp);
}
}
}
}
// Recurse
uint32_t count = ts_node_child_count(node);
for (uint32_t i = 0; i < count; i++) {
walk_wolfram_imports(ctx, ts_node_child(node, i));
}
}
static void parse_wolfram_imports(CBMExtractCtx* ctx) {
walk_wolfram_imports(ctx, ctx->root);
}
// --- Main dispatch ---
void cbm_extract_imports(CBMExtractCtx* ctx) {
@@ -618,6 +674,9 @@ void cbm_extract_imports(CBMExtractCtx* ctx) {
case CBM_LANG_MAGMA:
parse_generic_imports(ctx, "load_statement");
break;
case CBM_LANG_WOLFRAM:
parse_wolfram_imports(ctx);
break;
default:
break;
}
+20
View File
@@ -48,6 +48,26 @@ static void recompute_state(WalkState* state, const char* module_qn) {
// Compute function QN for scope tracking (mirrors cbm_enclosing_func_qn logic).
static const char* compute_func_qn(CBMExtractCtx* ctx, TSNode node,
const CBMLangSpec* spec, WalkState* state) {
// Wolfram: set_delayed_top/set_top/set_delayed/set — LHS is apply(user_symbol("f"), ...)
if (ctx->language == CBM_LANG_WOLFRAM) {
const char* nk = ts_node_type(node);
if (strcmp(nk, "set_delayed_top") == 0 || strcmp(nk, "set_top") == 0 ||
strcmp(nk, "set_delayed") == 0 || strcmp(nk, "set") == 0) {
if (ts_node_named_child_count(node) > 0) {
TSNode lhs = ts_node_named_child(node, 0);
if (strcmp(ts_node_type(lhs), "apply") == 0 && ts_node_named_child_count(lhs) > 0) {
TSNode head = ts_node_named_child(lhs, 0);
if (strcmp(ts_node_type(head), "user_symbol") == 0) {
char* name = cbm_node_text(ctx->arena, head, ctx->source);
if (name && name[0])
return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name);
}
}
}
return NULL;
}
}
TSNode name_node = ts_node_child_by_field_name(node, "name", 4);
// Arrow function: name from parent variable_declarator
+3
View File
@@ -0,0 +1,3 @@
// Vendored tree-sitter grammar: wolfram
#include "vendored/grammars/wolfram/parser.c"
#include "vendored/grammars/wolfram/scanner.c"
+19
View File
@@ -290,6 +290,7 @@ static const char* func_kinds_matlab[] = {"function_definition",NULL};
static const char* func_kinds_lean[] = {"def","theorem","instance","abbrev",NULL};
static const char* func_kinds_form[] = {"procedure_definition",NULL};
static const char* func_kinds_magma[] = {"function_definition","procedure_definition","intrinsic_definition",NULL};
static const char* func_kinds_wolfram[] = {"set_delayed_top","set_top","set_delayed","set",NULL};
static const char* func_kinds_generic[] = {"function_declaration","function_definition","method_declaration","method_definition",NULL};
static const char** func_kinds_for_lang(CBMLanguage lang) {
@@ -319,6 +320,7 @@ static const char** func_kinds_for_lang(CBMLanguage lang) {
case CBM_LANG_LEAN: return func_kinds_lean;
case CBM_LANG_FORM: return func_kinds_form;
case CBM_LANG_MAGMA: return func_kinds_magma;
case CBM_LANG_WOLFRAM: return func_kinds_wolfram;
default: return func_kinds_generic;
}
}
@@ -341,6 +343,23 @@ TSNode cbm_find_enclosing_func(TSNode node, CBMLanguage lang) {
// Get the name of a function node (basic: try "name" field)
static const char* func_node_name(CBMArena* a, TSNode func_node, const char* source, CBMLanguage lang) {
// Wolfram: set_delayed_top/set_top/set_delayed/set — LHS is apply(user_symbol("f"), ...)
if (lang == CBM_LANG_WOLFRAM) {
const char* nk = ts_node_type(func_node);
if (strcmp(nk, "set_delayed_top") == 0 || strcmp(nk, "set_top") == 0 ||
strcmp(nk, "set_delayed") == 0 || strcmp(nk, "set") == 0) {
if (ts_node_named_child_count(func_node) > 0) {
TSNode lhs = ts_node_named_child(func_node, 0);
if (strcmp(ts_node_type(lhs), "apply") == 0 && ts_node_named_child_count(lhs) > 0) {
TSNode head = ts_node_named_child(lhs, 0);
if (strcmp(ts_node_type(head), "user_symbol") == 0)
return cbm_node_text(a, head, source);
}
}
return NULL;
}
}
TSNode name_node = ts_node_child_by_field_name(func_node, "name", 4);
if (!ts_node_is_null(name_node)) {
return cbm_node_text(a, name_node, source);
+19 -4
View File
@@ -67,6 +67,7 @@ extern const TSLanguage* tree_sitter_matlab(void);
extern const TSLanguage* tree_sitter_lean(void);
extern const TSLanguage* tree_sitter_form(void);
extern const TSLanguage* tree_sitter_magma(void);
extern const TSLanguage* tree_sitter_wolfram(void);
// -- Empty sentinel --
static const char* empty_types[] = {NULL};
@@ -554,6 +555,7 @@ static const char* ini_var_types[] = {"setting",NULL};
static const char* matlab_func_types[] = {"function_definition",NULL};
static const char* matlab_class_types[] = {"class_definition",NULL};
static const char* matlab_module_types[] = {"source_file",NULL};
static const char* matlab_call_types[] = {"function_call","command",NULL};
static const char* matlab_branch_types[] = {"if_statement","for_statement","while_statement","switch_statement","try_statement",NULL};
static const char* matlab_var_types[] = {"assignment",NULL};
@@ -561,6 +563,7 @@ static const char* matlab_var_types[] = {"assignment",NULL};
static const char* lean_func_types[] = {"def","theorem","instance","abbrev",NULL};
static const char* lean_class_types[] = {"structure","class_inductive","inductive",NULL};
static const char* lean_module_types[] = {"module",NULL};
static const char* lean_call_types[] = {"apply",NULL};
static const char* lean_import_types[] = {"import",NULL};
static const char* lean_branch_types[] = {"if","match","do",NULL};
@@ -581,6 +584,12 @@ static const char* magma_import_types[] = {"load_statement",NULL};
static const char* magma_branch_types[] = {"if_statement","for_statement","while_statement","repeat_statement","case_statement",NULL};
static const char* magma_var_types[] = {"assignment_statement",NULL};
// ==================== WOLFRAM ====================
static const char* wolfram_func_types[] = {"set_delayed_top","set_top","set_delayed","set",NULL};
static const char* wolfram_module_types[] = {"source_file",NULL};
static const char* wolfram_call_types[] = {"apply",NULL};
static const char* wolfram_import_types[] = {"get_top",NULL};
// ==================== NEW LANG ENV ACCESS ====================
static const char* julia_env_funcs[] = {"ENV",NULL};
static const char* nix_env_funcs[] = {"builtins.getEnv",NULL};
@@ -885,13 +894,13 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
empty_types, empty_types, empty_types, ini_var_types, empty_types,
empty_types, NULL, empty_types, NULL, NULL},
// CBM_LANG_MATLAB (definitions-only, no call graph due to A(1) ambiguity)
{CBM_LANG_MATLAB, matlab_func_types, matlab_class_types, empty_types, matlab_module_types, empty_types,
// CBM_LANG_MATLAB
{CBM_LANG_MATLAB, matlab_func_types, matlab_class_types, empty_types, matlab_module_types, matlab_call_types,
empty_types, empty_types, matlab_branch_types, matlab_var_types, matlab_var_types,
empty_types, NULL, empty_types, NULL, NULL},
// CBM_LANG_LEAN (definitions-only, Lean 4 syntax is runtime-extensible)
{CBM_LANG_LEAN, lean_func_types, lean_class_types, empty_types, lean_module_types, empty_types,
// CBM_LANG_LEAN
{CBM_LANG_LEAN, lean_func_types, lean_class_types, empty_types, lean_module_types, lean_call_types,
lean_import_types, empty_types, lean_branch_types, empty_types, empty_types,
empty_types, NULL, empty_types, NULL, NULL},
@@ -904,6 +913,11 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
{CBM_LANG_MAGMA, magma_func_types, empty_types, empty_types, magma_module_types, magma_call_types,
magma_import_types, empty_types, magma_branch_types, magma_var_types, magma_var_types,
empty_types, NULL, empty_types, NULL, NULL},
// CBM_LANG_WOLFRAM
{CBM_LANG_WOLFRAM, wolfram_func_types, empty_types, empty_types, wolfram_module_types, wolfram_call_types,
wolfram_import_types, empty_types, empty_types, empty_types, empty_types,
empty_types, NULL, empty_types, NULL, NULL},
};
const CBMLangSpec* cbm_lang_spec(CBMLanguage lang) {
@@ -976,6 +990,7 @@ const TSLanguage* cbm_ts_language(CBMLanguage lang) {
case CBM_LANG_LEAN: return tree_sitter_lean();
case CBM_LANG_FORM: return tree_sitter_form();
case CBM_LANG_MAGMA: return tree_sitter_magma();
case CBM_LANG_WOLFRAM: return tree_sitter_wolfram();
default: return NULL;
}
}
+218
View File
@@ -999,6 +999,27 @@ func TestFORMParse_Regression(t *testing.T) {
}
}
func TestFORMCall_Regression(t *testing.T) {
src := []byte("#procedure myproc(x)\n id `x' = 0;\n#endprocedure\n#procedure caller()\n #call myproc(1)\n#endprocedure\n")
r, err := ExtractFile(src, lang.FORM, "t", "calc.frm")
if err != nil {
t.Fatal(err)
}
if len(r.Calls) == 0 {
t.Fatal("expected at least one call extracted from #call myproc(1), got none")
}
found := false
for _, c := range r.Calls {
if c.CalleeName == "myproc" {
found = true
break
}
}
if !found {
t.Fatalf("expected callee 'myproc' in calls, got: %+v", r.Calls)
}
}
// --- Magma ---
func TestMagmaFunction_Regression(t *testing.T) {
src := []byte("function Factorial(n)\n if n le 1 then\n return 1;\n end if;\n return n * Factorial(n - 1);\nend function;\n")
@@ -1062,3 +1083,200 @@ func TestMagmaCall_Regression(t *testing.T) {
t.Errorf("expected call to 'Bar', got calls: %v", r.Calls)
}
}
// --- MATLAB Calls ---
func TestMATLABCall_Regression(t *testing.T) {
src := []byte("function y = foo(x)\n y = inv(x);\n disp hello\nend\n")
r, err := ExtractFile(src, lang.MATLAB, "t", "foo.matlab")
if err != nil {
t.Fatal(err)
}
// Should extract function_call "inv" and command "disp"
if len(r.Calls) == 0 {
t.Fatal("expected calls to be extracted, got 0")
}
foundInv := false
foundDisp := false
for _, c := range r.Calls {
if c.CalleeName == "inv" {
foundInv = true
}
if c.CalleeName == "disp" {
foundDisp = true
}
}
if !foundInv {
t.Errorf("expected call to 'inv', got calls: %v", r.Calls)
}
if !foundDisp {
t.Errorf("expected call to 'disp', got calls: %v", r.Calls)
}
}
// --- Lean Calls ---
func TestLeanCall_Regression(t *testing.T) {
src := []byte("def fib : Nat → Nat\n | 0 => 1\n | 1 => 1\n | n + 2 => fib (n + 1) + fib n\n")
r, err := ExtractFile(src, lang.Lean, "t", "Fib.lean")
if err != nil {
t.Fatal(err)
}
if len(r.Calls) == 0 {
t.Fatal("expected calls to be extracted, got 0")
}
found := false
for _, c := range r.Calls {
if c.CalleeName == "fib" {
found = true
}
}
if !found {
t.Errorf("expected call to 'fib', got calls: %v", r.Calls)
}
}
func TestLeanTypeAnnotationNotCall_Regression(t *testing.T) {
// "List Nat" in a binder type should not produce a "List" call.
// "IO.println" in the body should be extracted.
src := []byte("def listLen (xs : List Nat) : Nat := 0\ndef greet : IO Unit := IO.println \"hi\"\n")
r, err := ExtractFile(src, lang.Lean, "t", "Types.lean")
if err != nil {
t.Fatal(err)
}
// "List" in explicit_binder type should be filtered
for _, c := range r.Calls {
if c.CalleeName == "List" {
t.Errorf("type-position apply %q (binder type) should not be extracted as a call", c.CalleeName)
}
}
// IO.println in the body should be present
foundPrintln := false
for _, c := range r.Calls {
if c.CalleeName == "IO.println" || c.CalleeName == "println" {
foundPrintln = true
}
}
if !foundPrintln {
t.Errorf("expected call to IO.println/println, got calls: %v", r.Calls)
}
}
// --- Wolfram ---
func TestWolframFunction_Regression(t *testing.T) {
src := []byte("f[x_] := x^2\ng[x_] = x + 1\n")
r, err := ExtractFile(src, lang.Wolfram, "t", "funcs.wl")
if err != nil {
t.Fatal(err)
}
fns := defsWithLabel(r, "Function")
if len(fns) < 2 {
t.Fatalf("expected at least 2 functions, got %d: %v", len(fns), fns)
}
assertHasName(t, fns, "f")
assertHasName(t, fns, "g")
}
func TestWolframCall_Regression(t *testing.T) {
src := []byte("f[x_] := g[x] + h[x]\n")
r, err := ExtractFile(src, lang.Wolfram, "t", "calls.wl")
if err != nil {
t.Fatal(err)
}
if len(r.Calls) == 0 {
t.Fatal("expected calls to be extracted, got 0")
}
foundG := false
foundH := false
for _, c := range r.Calls {
if c.CalleeName == "g" {
foundG = true
}
if c.CalleeName == "h" {
foundH = true
}
}
if !foundG {
t.Errorf("expected call to 'g', got calls: %v", r.Calls)
}
if !foundH {
t.Errorf("expected call to 'h', got calls: %v", r.Calls)
}
// "f" should NOT appear as a call (it's the definition LHS)
for _, c := range r.Calls {
if c.CalleeName == "f" {
t.Errorf("'f' should not be a call (it's a definition LHS), but found in calls")
}
}
}
func TestWolframCallerAttribution_Regression(t *testing.T) {
// Calls inside a function body should have the enclosing function as caller,
// not the file path (module QN). This verifies func_kinds_wolfram is wired
// into func_kinds_for_lang so cbm_find_enclosing_func returns the def node.
src := []byte("f[x_] := g[x] + h[x]\n")
r, err := ExtractFile(src, lang.Wolfram, "t", "caller.wl")
if err != nil {
t.Fatal(err)
}
if len(r.Calls) == 0 {
t.Fatal("expected calls, got 0")
}
for _, c := range r.Calls {
if c.CalleeName == "g" || c.CalleeName == "h" {
// enclosing_func_qn must NOT be the module path (file path)
if c.EnclosingFuncQN == "" || c.EnclosingFuncQN == "t.caller" {
t.Errorf("call to %q: enclosing_func_qn is file path %q, want function QN", c.CalleeName, c.EnclosingFuncQN)
}
}
}
}
func TestWolframParse_Regression(t *testing.T) {
src := []byte("x = 42;\ny = x + 1;\n")
_, err := ExtractFile(src, lang.Wolfram, "t", "simple.wl")
if err != nil {
t.Fatal(err)
}
}
func TestWolframImport_Regression(t *testing.T) {
src := []byte("<< \"utils.wl\"\nNeeds[\"Package`\"]\n")
r, err := ExtractFile(src, lang.Wolfram, "t", "main.wl")
if err != nil {
t.Fatal(err)
}
if len(r.Imports) == 0 {
t.Fatalf("expected imports, got 0")
}
}
func TestWolframNestedDef_Regression(t *testing.T) {
// set_delayed (non-top) inside Module should also be extracted
src := []byte("main[x_] := Module[{localF}, localF[t_] := t + 1; localF[x]]\n")
r, err := ExtractFile(src, lang.Wolfram, "t", "nested.wl")
if err != nil {
t.Fatal(err)
}
fns := defsWithLabel(r, "Function")
assertHasName(t, fns, "main")
assertHasName(t, fns, "localF")
}
func TestMagmaDisambiguation_Regression(t *testing.T) {
// Magma code with 'end function;' should be extractable as Magma
src := []byte(`function Factorial(n)
if n le 1 then
return 1;
end if;
return n * Factorial(n - 1);
end function;
`)
r, err := ExtractFile(src, lang.Magma, "t", "test.m")
if err != nil {
t.Fatal(err)
}
fns := defsWithLabel(r, "Function")
if len(fns) == 0 {
t.Fatal("expected at least 1 function from Magma code")
}
assertHasName(t, fns, "Factorial")
}
File diff suppressed because it is too large. Load diff
+68
View File
@@ -0,0 +1,68 @@
#include "tree_sitter/parser.h"
#include <wctype.h>
enum TokenType {
COMMENT,
};
void *tree_sitter_wolfram_external_scanner_create() { return NULL; }
void tree_sitter_wolfram_external_scanner_destroy(void *p) {}
void tree_sitter_wolfram_external_scanner_reset(void *p) {}
unsigned tree_sitter_wolfram_external_scanner_serialize(void *p, char *buffer) { return 0; }
void tree_sitter_wolfram_external_scanner_deserialize(void *p, const char *b, unsigned n) {}
static void advance(TSLexer *lexer) {
lexer->advance(lexer, false);
}
bool tree_sitter_wolfram_external_scanner_scan(void *payload, TSLexer *lexer,
const bool *valid_symbols) {
while (iswspace(lexer->lookahead)) lexer->advance(lexer, true);
if (lexer->lookahead == '(') {
advance(lexer);
if (lexer->lookahead != '*') return false;
advance(lexer);
bool after_star = false;
unsigned nesting_depth = 1;
for (;;) {
switch (lexer->lookahead) {
case '\0':
return false;
case '*':
advance(lexer);
after_star = true;
break;
case '(':
advance(lexer);
after_star = false;
if (lexer->lookahead == '*') {
nesting_depth++;
advance(lexer);
}
break;
case ')':
if (after_star) {
advance(lexer);
after_star = false;
nesting_depth--;
if (nesting_depth == 0) {
lexer->result_symbol = COMMENT;
return true;
}
} else {
advance(lexer);
after_star = false;
}
break;
default:
advance(lexer);
after_star = false;
break;
}
}
}
return false;
}
@@ -0,0 +1,224 @@
#ifndef TREE_SITTER_PARSER_H_
#define TREE_SITTER_PARSER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define ts_builtin_sym_error ((TSSymbol)-1)
#define ts_builtin_sym_end 0
#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
typedef uint16_t TSStateId;
#ifndef TREE_SITTER_API_H_
typedef uint16_t TSSymbol;
typedef uint16_t TSFieldId;
typedef struct TSLanguage TSLanguage;
#endif
typedef struct {
TSFieldId field_id;
uint8_t child_index;
bool inherited;
} TSFieldMapEntry;
typedef struct {
uint16_t index;
uint16_t length;
} TSFieldMapSlice;
typedef struct {
bool visible;
bool named;
bool supertype;
} TSSymbolMetadata;
typedef struct TSLexer TSLexer;
struct TSLexer {
int32_t lookahead;
TSSymbol result_symbol;
void (*advance)(TSLexer *, bool);
void (*mark_end)(TSLexer *);
uint32_t (*get_column)(TSLexer *);
bool (*is_at_included_range_start)(const TSLexer *);
bool (*eof)(const TSLexer *);
};
typedef enum {
TSParseActionTypeShift,
TSParseActionTypeReduce,
TSParseActionTypeAccept,
TSParseActionTypeRecover,
} TSParseActionType;
typedef union {
struct {
uint8_t type;
TSStateId state;
bool extra;
bool repetition;
} shift;
struct {
uint8_t type;
uint8_t child_count;
TSSymbol symbol;
int16_t dynamic_precedence;
uint16_t production_id;
} reduce;
uint8_t type;
} TSParseAction;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
} TSLexMode;
typedef union {
TSParseAction action;
struct {
uint8_t count;
bool reusable;
} entry;
} TSParseActionEntry;
struct TSLanguage {
uint32_t version;
uint32_t symbol_count;
uint32_t alias_count;
uint32_t token_count;
uint32_t external_token_count;
uint32_t state_count;
uint32_t large_state_count;
uint32_t production_id_count;
uint32_t field_count;
uint16_t max_alias_sequence_length;
const uint16_t *parse_table;
const uint16_t *small_parse_table;
const uint32_t *small_parse_table_map;
const TSParseActionEntry *parse_actions;
const char * const *symbol_names;
const char * const *field_names;
const TSFieldMapSlice *field_map_slices;
const TSFieldMapEntry *field_map_entries;
const TSSymbolMetadata *symbol_metadata;
const TSSymbol *public_symbol_map;
const uint16_t *alias_map;
const TSSymbol *alias_sequences;
const TSLexMode *lex_modes;
bool (*lex_fn)(TSLexer *, TSStateId);
bool (*keyword_lex_fn)(TSLexer *, TSStateId);
TSSymbol keyword_capture_token;
struct {
const bool *states;
const TSSymbol *symbol_map;
void *(*create)(void);
void (*destroy)(void *);
bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
unsigned (*serialize)(void *, char *);
void (*deserialize)(void *, const char *, unsigned);
} external_scanner;
const TSStateId *primary_state_ids;
};
/*
* Lexer Macros
*/
#define START_LEXER() \
bool result = false; \
bool skip = false; \
bool eof = false; \
int32_t lookahead; \
goto start; \
next_state: \
lexer->advance(lexer, skip); \
start: \
skip = false; \
lookahead = lexer->lookahead;
#define ADVANCE(state_value) \
{ \
state = state_value; \
goto next_state; \
}
#define SKIP(state_value) \
{ \
skip = true; \
state = state_value; \
goto next_state; \
}
#define ACCEPT_TOKEN(symbol_value) \
result = true; \
lexer->result_symbol = symbol_value; \
lexer->mark_end(lexer);
#define END_STATE() return result;
/*
* Parse Table Macros
*/
#define SMALL_STATE(id) id - LARGE_STATE_COUNT
#define STATE(id) id
#define ACTIONS(id) id
#define SHIFT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value \
} \
}}
#define SHIFT_REPEAT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value, \
.repetition = true \
} \
}}
#define SHIFT_EXTRA() \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.extra = true \
} \
}}
#define REDUCE(symbol_val, child_count_val, ...) \
{{ \
.reduce = { \
.type = TSParseActionTypeReduce, \
.symbol = symbol_val, \
.child_count = child_count_val, \
__VA_ARGS__ \
}, \
}}
#define RECOVER() \
{{ \
.type = TSParseActionTypeRecover \
}}
#define ACCEPT_INPUT() \
{{ \
.type = TSParseActionTypeAccept \
}}
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_PARSER_H_
+8 -1
View File
@@ -214,7 +214,10 @@ var objcMarkers = regexp.MustCompile(`@interface|@implementation|@protocol|@prop
// MATLAB markers adapted from GitHub Linguist heuristics.yml
var matlabMarkers = regexp.MustCompile(`(?m)^\s*function\b|^\s*classdef\b|^\s*%%|^\s*%[^{]`)
// disambiguateM determines whether a .m file is MATLAB or Objective-C.
// Magma markers — semicolons after end-blocks and procedure/intrinsic keywords are unique to Magma
var magmaMarkers = regexp.MustCompile(`(?m)end\s+(function|procedure|intrinsic)\s*;|\bintrinsic\s+\w+\s*\(|\bprocedure\s+\w+\s*\(|end\s+if\s*;|end\s+for\s*;|end\s+while\s*;`)
// disambiguateM determines whether a .m file is MATLAB, Objective-C, or Magma.
// Returns the detected language and true, or ("", false) if undetermined.
func disambiguateM(path string) (lang.Language, bool) {
f, err := os.Open(path)
@@ -234,6 +237,10 @@ func disambiguateM(path string) (lang.Language, bool) {
if objcMarkers.MatchString(head) {
return lang.ObjectiveC, true
}
// Check Magma before MATLAB — both use 'function' keyword but Magma has 'end function;'
if magmaMarkers.MatchString(head) {
return lang.Magma, true
}
if matlabMarkers.MatchString(head) {
return lang.MATLAB, true
}
+36
View File
@@ -6,6 +6,8 @@ import (
"os"
"path/filepath"
"testing"
"github.com/DeusData/codebase-memory-mcp/internal/lang"
)
func TestDiscoverBasic(t *testing.T) {
@@ -43,6 +45,40 @@ func TestDiscoverBasic(t *testing.T) {
}
}
func TestDisambiguateM(t *testing.T) {
dir := t.TempDir()
tests := []struct {
name string
content string
want lang.Language
}{
{"objc", "@interface Foo\n@end\n", lang.ObjectiveC},
{"matlab_function", "function y = foo(x)\n y = x^2;\nend\n", lang.MATLAB},
{"matlab_comment", "% This is MATLAB\nx = 1;\n", lang.MATLAB},
{"magma_end_function", "function Fact(n)\n return n;\nend function;\n", lang.Magma},
{"magma_procedure", "procedure DoStuff(~x)\n x := 1;\nend procedure;\n", lang.Magma},
{"magma_intrinsic", "intrinsic IsSmall(x :: RngIntElt) -> BoolElt\n", lang.Magma},
{"magma_end_if", "if n le 1 then\n return 1;\nend if;\n", lang.Magma},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := filepath.Join(dir, tt.name+".m")
if err := os.WriteFile(path, []byte(tt.content), 0o600); err != nil {
t.Fatal(err)
}
got, ok := disambiguateM(path)
if !ok {
t.Fatal("disambiguateM returned ok=false")
}
if got != tt.want {
t.Errorf("disambiguateM(%s) = %s, want %s", tt.name, got, tt.want)
}
})
}
}
func TestDiscoverCancellation(t *testing.T) {
dir := t.TempDir()
+6 -5
View File
@@ -47,10 +47,11 @@ const (
EmacsLisp Language = "emacslisp"
// Scientific/math languages
MATLAB Language = "matlab"
Lean Language = "lean"
FORM Language = "form"
Magma Language = "magma"
MATLAB Language = "matlab"
Lean Language = "lean"
FORM Language = "form"
Magma Language = "magma"
Wolfram Language = "wolfram"
// Helper languages (Tier 2)
HTML Language = "html"
@@ -83,7 +84,7 @@ func AllLanguages() []Language {
Swift, Dart, Perl, Groovy, Erlang, R,
Clojure, FSharp, Julia, VimScript, Nix, CommonLisp, Elm, Fortran,
CUDA, COBOL, Verilog, EmacsLisp,
MATLAB, Lean, FORM, Magma,
MATLAB, Lean, FORM, Magma, Wolfram,
HTML, CSS, SCSS, YAML, TOML, HCL, SQL, Dockerfile,
JSON, XML, Markdown, Makefile, CMake, Protobuf, GraphQL,
Vue, Svelte, Meson, GLSL, INI,
+1 -1
View File
@@ -7,7 +7,7 @@ func init() {
FunctionNodeTypes: []string{"def", "theorem", "instance", "abbrev"},
ClassNodeTypes: []string{"structure", "class_inductive", "inductive"},
ModuleNodeTypes: []string{"module"},
CallNodeTypes: []string{},
CallNodeTypes: []string{"apply"},
ImportNodeTypes: []string{"import"},
BranchingNodeTypes: []string{"if", "match", "do"},
VariableNodeTypes: []string{},
+1 -1
View File
@@ -7,7 +7,7 @@ func init() {
FunctionNodeTypes: []string{"function_definition"},
ClassNodeTypes: []string{"class_definition"},
ModuleNodeTypes: []string{"source_file"},
CallNodeTypes: []string{},
CallNodeTypes: []string{"function_call", "command"},
ImportNodeTypes: []string{},
BranchingNodeTypes: []string{"if_statement", "for_statement", "while_statement", "switch_statement", "try_statement"},
VariableNodeTypes: []string{"assignment"},
+12
View File
@@ -0,0 +1,12 @@
package lang
func init() {
Register(&LanguageSpec{
Language: Wolfram,
FileExtensions: []string{".wl", ".wls"},
FunctionNodeTypes: []string{"set_delayed_top", "set_top", "set_delayed", "set"},
ModuleNodeTypes: []string{"source_file"},
CallNodeTypes: []string{"apply"},
ImportNodeTypes: []string{"get_top"},
})
}
+51
View File
@@ -814,3 +814,54 @@ func TestProjectNameUniqueness(t *testing.T) {
t.Errorf("collision: %q == %q", a, b)
}
}
func TestFORMProcedureCallResolution(t *testing.T) {
dir, err := os.MkdirTemp("", "cbm-form-test-*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
// Two procedures: caller calls callee via #call
writeFile(t, filepath.Join(dir, "calc.frm"), `#procedure callee(x)
id x = 0;
#endprocedure
#procedure caller()
#call callee(1)
#endprocedure
`)
s, err := store.OpenMemory()
if err != nil {
t.Fatal(err)
}
defer s.Close()
p := New(context.Background(), s, dir, discover.ModeFull)
if err := p.Run(); err != nil {
t.Fatalf("Pipeline.Run: %v", err)
}
funcs, _ := s.FindNodesByLabel(p.ProjectName, "Function")
t.Logf("FORM functions: %d", len(funcs))
for _, f := range funcs {
t.Logf(" func: %s (%s)", f.Name, f.QualifiedName)
}
edges, _ := s.FindEdgesByType(p.ProjectName, "CALLS")
t.Logf("CALLS edges: %d", len(edges))
found := false
for _, e := range edges {
src, _ := s.FindNodeByID(e.SourceID)
tgt, _ := s.FindNodeByID(e.TargetID)
if src != nil && tgt != nil {
t.Logf(" CALLS: %s -> %s", src.Name, tgt.Name)
}
if tgt != nil && tgt.Name == "callee" {
found = true
}
}
if !found {
t.Error("expected a CALLS edge from caller to callee, got none")
}
}