mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
ced21194ac
commit
497dd6e61a
21 files changed
+115082
-14
No files matched your search
@@ -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
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vendored tree-sitter grammar: wolfram
|
||||
#include "vendored/grammars/wolfram/parser.c"
|
||||
#include "vendored/grammars/wolfram/scanner.c"
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
+114240
File diff suppressed because it is too large.
Load diff
+68
@@ -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_
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"},
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user