diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 854352ac0..0a637238a 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -3,6 +3,8 @@ #include "lang_specs.h" #include "extract_unified.h" #include "lsp/go_lsp.h" +#include "lsp/c_lsp.h" +#include "preprocessor.h" #include #include #include @@ -12,6 +14,9 @@ #include static _Atomic uint64_t total_parse_ns = 0; static _Atomic uint64_t total_extract_ns = 0; +static _Atomic uint64_t total_lsp_ns = 0; +static _Atomic uint64_t total_preprocess_ns = 0; +static _Atomic uint64_t total_files_preprocessed = 0; static _Atomic uint64_t total_files = 0; static uint64_t now_ns(void) { @@ -27,10 +32,25 @@ void cbm_get_profile(uint64_t* parse_ns, uint64_t* extract_ns, uint64_t* files) *files = atomic_load(&total_files); } +uint64_t cbm_get_lsp_ns(void) { + return atomic_load(&total_lsp_ns); +} + +uint64_t cbm_get_preprocess_ns(void) { + return atomic_load(&total_preprocess_ns); +} + +uint64_t cbm_get_files_preprocessed(void) { + return atomic_load(&total_files_preprocessed); +} + // cbm_reset_profile zeros the profiling counters. void cbm_reset_profile(void) { atomic_store(&total_parse_ns, 0); atomic_store(&total_extract_ns, 0); + atomic_store(&total_lsp_ns, 0); + atomic_store(&total_preprocess_ns, 0); + atomic_store(&total_files_preprocessed, 0); atomic_store(&total_files, 0); } @@ -178,7 +198,9 @@ CBMFileResult* cbm_extract_file( const char* source, int source_len, CBMLanguage language, const char* project, const char* rel_path, - int64_t timeout_micros + int64_t timeout_micros, + const char** extra_defines, + const char** include_paths ) { // Allocate result on heap (arena inside for all string data) CBMFileResult* result = (CBMFileResult*)calloc(1, sizeof(CBMFileResult)); @@ -267,10 +289,75 @@ CBMFileResult* cbm_extract_file( cbm_extract_imports(&ctx); cbm_extract_unified(&ctx); - // LSP type-aware call resolution (Go only for now) + // LSP type-aware call resolution + uint64_t lsp_start = now_ns(); if (language == CBM_LANG_GO) { cbm_run_go_lsp(a, result, source, source_len, root); } + if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) { + cbm_run_c_lsp(a, result, source, source_len, root, language != CBM_LANG_C); + } + atomic_fetch_add(&total_lsp_ns, now_ns() - lsp_start); + + // Second pass: preprocess C/C++/CUDA and extract additional macro-hidden calls. + // Defs keep original-source line numbers; only CALLS are extracted from expanded source. + if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) { + uint64_t pp_start = now_ns(); + char* expanded = cbm_preprocess(source, source_len, rel_path, + extra_defines, include_paths, + language != CBM_LANG_C); + if (expanded) { + int expanded_len = (int)strlen(expanded); + // Record calls count before second pass + int calls_before = result->calls.count; + + // Parse expanded source with fresh tree + TSParser* pp_parser = get_thread_parser(ts_lang, language); + if (pp_parser) { + ts_parser_reset(pp_parser); + CBMStringInput pp_input = {expanded, (uint32_t)expanded_len}; + TSInput pp_ts_input = { + &pp_input, + cbm_string_read, + TSInputEncodingUTF8, + NULL, + }; + TSParseOptions pp_opts = {0}; + TSTree* pp_tree = ts_parser_parse_with_options(pp_parser, NULL, pp_ts_input, pp_opts); + if (pp_tree) { + TSNode pp_root = ts_tree_root_node(pp_tree); + + // Build context for expanded source — extract only calls via unified extractor + CBMExtractCtx pp_ctx = { + .arena = a, + .result = result, + .source = expanded, + .source_len = expanded_len, + .language = language, + .project = project, + .rel_path = rel_path, + .module_qn = result->module_qn, + .root = pp_root, + }; + // Re-run unified extraction on expanded source. + // This adds macro-expanded calls; duplicates with original calls are + // harmless (pipeline deduplicates by caller+callee). + cbm_extract_unified(&pp_ctx); + + // Also run LSP on expanded source for additional type-resolved calls + if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) { + cbm_run_c_lsp(a, result, expanded, expanded_len, pp_root, language != CBM_LANG_C); + } + + ts_tree_delete(pp_tree); + } + } + cbm_preprocess_free(expanded); + atomic_fetch_add(&total_files_preprocessed, 1); + (void)calls_before; // used for future logging + } + atomic_fetch_add(&total_preprocess_ns, now_ns() - pp_start); + } uint64_t t2 = now_ns(); diff --git a/internal/cbm/cbm.go b/internal/cbm/cbm.go index d95ad92e1..6740f3a83 100644 --- a/internal/cbm/cbm.go +++ b/internal/cbm/cbm.go @@ -2,6 +2,9 @@ package cbm /* #cgo CFLAGS: -std=c11 -D_DEFAULT_SOURCE -O2 -Wall -Ivendored/ts_runtime/include -Ivendored/ts_runtime/src +#cgo CXXFLAGS: -std=c++17 -O2 -Wall -Ivendored/ts_runtime/include +#cgo darwin LDFLAGS: -lc++ +#cgo linux LDFLAGS: -lstdc++ #include "cbm.h" #include "helpers.h" #include "lang_specs.h" @@ -143,17 +146,30 @@ func Shutdown() { // ProfileStats holds accumulated profiling data from C extraction. type ProfileStats struct { - ParseNs uint64 - ExtractNs uint64 - Files uint64 + ParseNs uint64 + ExtractNs uint64 + LspNs uint64 + PreprocessNs uint64 + FilesPreprocessed uint64 + Files uint64 } // GetProfile returns accumulated parse/extraction timing and resets counters. func GetProfile() ProfileStats { var p, e, f C.uint64_t C.cbm_get_profile(&p, &e, &f) + lsp := uint64(C.cbm_get_lsp_ns()) + ppNs := uint64(C.cbm_get_preprocess_ns()) + ppFiles := uint64(C.cbm_get_files_preprocessed()) C.cbm_reset_profile() - return ProfileStats{ParseNs: uint64(p), ExtractNs: uint64(e), Files: uint64(f)} + return ProfileStats{ + ParseNs: uint64(p), + ExtractNs: uint64(e), + LspNs: lsp, + PreprocessNs: ppNs, + FilesPreprocessed: ppFiles, + Files: uint64(f), + } } // languageToC maps Go lang.Language to CBMLanguage enum. @@ -229,10 +245,21 @@ var languageToC = map[lang.Language]C.CBMLanguage{ // ParseTimeoutMicros is the default per-file parse timeout (10 seconds). const ParseTimeoutMicros = 10_000_000 +// ExtractionFlags holds optional compile flags for C/C++ preprocessing. +type ExtractionFlags struct { + IncludePaths []string // -I paths for #include resolution + Defines []string // -D defines as "NAME=VALUE" or "NAME" +} + // ExtractFile runs the C extraction library on one file. // source is the file content, language is the programming language, // project and relPath are used for qualified name computation. func ExtractFile(source []byte, language lang.Language, project, relPath string) (*FileResult, error) { + return ExtractFileWithFlags(source, language, project, relPath, nil) +} + +// ExtractFileWithFlags runs extraction with optional compile flags for C/C++ preprocessing. +func ExtractFileWithFlags(source []byte, language lang.Language, project, relPath string, flags *ExtractionFlags) (*FileResult, error) { Init() if len(source) == 0 { @@ -251,7 +278,27 @@ func ExtractFile(source []byte, language lang.Language, project, relPath string) defer C.free(unsafe.Pointer(cProject)) defer C.free(unsafe.Pointer(cRelPath)) - result := C.cbm_extract_file(cSource, cSourceLen, cLang, cProject, cRelPath, C.int64_t(ParseTimeoutMicros)) + // Build C string arrays for defines and include paths + var cDefines **C.char + var cIncludes **C.char + var toFree []unsafe.Pointer + + if flags != nil { + if len(flags.Defines) > 0 { + cDefines = buildCStringArray(flags.Defines, &toFree) + } + if len(flags.IncludePaths) > 0 { + cIncludes = buildCStringArray(flags.IncludePaths, &toFree) + } + } + defer func() { + for _, p := range toFree { + C.free(p) + } + }() + + result := C.cbm_extract_file(cSource, cSourceLen, cLang, cProject, cRelPath, + C.int64_t(ParseTimeoutMicros), cDefines, cIncludes) if result == nil { return nil, fmt.Errorf("cbm: extraction returned nil") } @@ -264,6 +311,23 @@ func ExtractFile(source []byte, language lang.Language, project, relPath string) return convertResult(result), nil } +// buildCStringArray creates a NULL-terminated C string array from Go strings. +func buildCStringArray(strs []string, toFree *[]unsafe.Pointer) **C.char { + n := len(strs) + ptrSize := C.size_t(unsafe.Sizeof((*C.char)(nil))) + arr := (**C.char)(C.malloc(C.size_t(n+1) * ptrSize)) + *toFree = append(*toFree, unsafe.Pointer(arr)) + + slice := unsafe.Slice(arr, n+1) + for i, s := range strs { + cs := C.CString(s) + *toFree = append(*toFree, unsafe.Pointer(cs)) + slice[i] = cs + } + slice[n] = nil // NULL terminator + return arr +} + func convertResult(r *C.CBMFileResult) *FileResult { fr := &FileResult{ ModuleQN: C.GoString(r.module_qn), diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index a9a338bad..18b254a17 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -300,7 +300,9 @@ CBMFileResult* cbm_extract_file( const char* source, int source_len, CBMLanguage language, const char* project, const char* rel_path, - int64_t timeout_micros + int64_t timeout_micros, + const char** extra_defines, // NULL-terminated, or NULL + const char** include_paths // NULL-terminated, or NULL ); // Free all memory associated with a result. @@ -311,6 +313,9 @@ void cbm_shutdown(void); // Profiling: get accumulated parse/extraction times and file count. void cbm_get_profile(uint64_t* parse_ns, uint64_t* extract_ns, uint64_t* files); +uint64_t cbm_get_lsp_ns(void); +uint64_t cbm_get_preprocess_ns(void); +uint64_t cbm_get_files_preprocessed(void); void cbm_reset_profile(void); diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index e95b60c47..64c1aea00 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -11,6 +11,7 @@ static void extract_variables(CBMExtractCtx* ctx, TSNode root, const CBMLangSpec static void extract_class_variables(CBMExtractCtx* ctx, TSNode class_node, const CBMLangSpec* spec); static void extract_rust_impl(CBMExtractCtx* ctx, TSNode node, const CBMLangSpec* spec); static void extract_class_methods(CBMExtractCtx* ctx, TSNode class_node, const char* class_qn, const CBMLangSpec* spec); +static void extract_class_fields(CBMExtractCtx* ctx, TSNode class_node, const char* class_qn, const CBMLangSpec* spec); static void extract_elixir_call(CBMExtractCtx* ctx, TSNode node, const CBMLangSpec* spec); // --- Helpers --- @@ -252,6 +253,23 @@ static TSNode resolve_func_name(TSNode node, CBMLanguage lang, const char* sourc return null_node; } + // C++/CUDA: template_declaration wraps a function_definition — unwrap and resolve inner + if ((lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) && + strcmp(kind, "template_declaration") == 0) { + // Find the inner function_definition or declaration child + uint32_t nc = ts_node_named_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode ch = ts_node_named_child(node, i); + const char* ck = ts_node_type(ch); + if (strcmp(ck, "function_definition") == 0 || + strcmp(ck, "declaration") == 0) { + return resolve_func_name(ch, lang, source); + } + } + 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 || @@ -260,11 +278,25 @@ static TSNode resolve_func_name(TSNode node, CBMLanguage lang, const char* sourc for (int depth = 0; depth < 8 && !ts_node_is_null(decl); depth++) { const char* dk = ts_node_type(decl); if (strcmp(dk, "identifier") == 0) return decl; - // C++ qualified name: Namespace::Function + if (strcmp(dk, "field_identifier") == 0) return decl; + // C++ operator functions: operator+, operator[], operator(), etc. + if (strcmp(dk, "operator_name") == 0 || strcmp(dk, "operator_cast") == 0) return decl; + // C++ destructor: ~ClassName + if (strcmp(dk, "destructor_name") == 0) return decl; + // C++ qualified name: Namespace::Function or Class::operator+ if (strcmp(dk, "qualified_identifier") == 0 || strcmp(dk, "scoped_identifier") == 0) { + // Check for operator_name child first (e.g., Class::operator+) + TSNode op = cbm_find_child_by_kind(decl, "operator_name"); + if (!ts_node_is_null(op)) return op; + op = cbm_find_child_by_kind(decl, "operator_cast"); + if (!ts_node_is_null(op)) return op; + op = cbm_find_child_by_kind(decl, "destructor_name"); + if (!ts_node_is_null(op)) return op; TSNode id = cbm_find_child_by_kind(decl, "identifier"); if (!ts_node_is_null(id)) return id; + id = cbm_find_child_by_kind(decl, "field_identifier"); + if (!ts_node_is_null(id)) return id; break; } // Unwrap pointer_declarator, reference_declarator, function_declarator, etc. @@ -770,6 +802,23 @@ static void extract_func_def(CBMExtractCtx* ctx, TSNode node, const CBMLangSpec* char* name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0] || strcmp(name, "function") == 0) return; + // For template_declaration, use the inner function_definition for field lookups + // (parameters, return type, etc. are on the inner node, not the template wrapper) + TSNode func_node = node; + if ((ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA) && + strcmp(ts_node_type(node), "template_declaration") == 0) { + uint32_t nc = ts_node_named_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode ch = ts_node_named_child(node, i); + const char* ck = ts_node_type(ch); + if (strcmp(ck, "function_definition") == 0 || + strcmp(ck, "declaration") == 0) { + func_node = ch; + break; + } + } + } + CBMDefinition def; memset(&def, 0, sizeof(def)); @@ -782,18 +831,28 @@ static void extract_func_def(CBMExtractCtx* ctx, TSNode node, const CBMLangSpec* def.lines = (int)(def.end_line - def.start_line + 1); def.is_exported = cbm_is_exported(name, ctx->language); - // Parameters - TSNode params = ts_node_child_by_field_name(node, "parameters", 10); + // Parameters — use func_node (inner function for templates) + TSNode params = ts_node_child_by_field_name(func_node, "parameters", 10); + // C/C++/CUDA/GLSL: parameters live on function_declarator inside declarator chain + if (ts_node_is_null(params) && (ctx->language == CBM_LANG_C || ctx->language == CBM_LANG_CPP || + ctx->language == CBM_LANG_CUDA || ctx->language == CBM_LANG_GLSL)) { + TSNode decl = ts_node_child_by_field_name(func_node, "declarator", 10); + for (int d = 0; d < 5 && !ts_node_is_null(decl); d++) { + params = ts_node_child_by_field_name(decl, "parameters", 10); + if (!ts_node_is_null(params)) break; + decl = ts_node_child_by_field_name(decl, "declarator", 10); + } + } if (!ts_node_is_null(params)) { def.signature = cbm_node_text(a, params, ctx->source); def.param_names = extract_param_names(a, params, ctx->source, ctx->language); def.param_types = extract_param_types(a, params, ctx->source, ctx->language); } - // Return type + // Return type — use func_node (inner function for templates) static const char* rt_fields[] = {"result","return_type","type",NULL}; for (const char** f = rt_fields; *f; f++) { - TSNode rt = ts_node_child_by_field_name(node, *f, (uint32_t)strlen(*f)); + TSNode rt = ts_node_child_by_field_name(func_node, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(rt)) { def.return_type = cbm_node_text(a, rt, ctx->source); def.return_types = extract_return_types(a, rt, ctx->source, ctx->language); @@ -801,6 +860,26 @@ static void extract_func_def(CBMExtractCtx* ctx, TSNode node, const CBMLangSpec* } } + // C++: trailing return type (auto f() -> Type) — override "auto" with actual type + if (def.return_type && strcmp(def.return_type, "auto") == 0 && + (ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA)) { + TSNode declarator = ts_node_child_by_field_name(func_node, "declarator", 10); + if (!ts_node_is_null(declarator)) { + // trailing_return_type is a child of the function_declarator + uint32_t nc = ts_node_named_child_count(declarator); + for (uint32_t i = 0; i < nc; i++) { + TSNode ch = ts_node_named_child(declarator, i); + if (strcmp(ts_node_type(ch), "trailing_return_type") == 0) { + // trailing_return_type contains a type_descriptor as first named child + TSNode type_desc = ts_node_named_child_count(ch) > 0 + ? ts_node_named_child(ch, 0) : ch; + def.return_type = cbm_node_text(a, type_desc, ctx->source); + break; + } + } + } + } + // Receiver (Go methods) TSNode recv = ts_node_child_by_field_name(node, "receiver", 8); if (!ts_node_is_null(recv)) { @@ -1064,6 +1143,9 @@ static void extract_class_def(CBMExtractCtx* ctx, TSNode node, const CBMLangSpec // Extract methods inside the class extract_class_methods(ctx, node, class_qn, spec); + // Extract typed struct/class fields (for cross-file LSP type resolution) + extract_class_fields(ctx, node, class_qn, spec); + // Extract class-level variables (field declarations) extract_class_variables(ctx, node, spec); } @@ -1112,6 +1194,13 @@ static TSNode resolve_method_name(TSNode child, CBMLanguage lang) { const char* ck = ts_node_type(child); + // C/C++/CUDA: function_definition inside class body — use resolve_func_name which + // handles the declarator chain including operator_name, destructor_name, etc. + if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA || + lang == CBM_LANG_GLSL) && strcmp(ck, "function_definition") == 0) { + return resolve_func_name(child, lang, NULL); + } + // Groovy: function_definition uses field_function for the method name (not field_name). // e.g., "String greet()" → {field_type:String, field_function:greet, ...} // Using the first identifier would pick up the return type instead of the name. @@ -1191,6 +1280,36 @@ static void push_method_def(CBMExtractCtx* ctx, TSNode child, const char* class_ def.param_types = extract_param_types(a, params, ctx->source, ctx->language); } + // Return type (same fields as extract_func_def) + { + static const char* rt_fields[] = {"result","return_type","type",NULL}; + for (const char** f = rt_fields; *f; f++) { + TSNode rt = ts_node_child_by_field_name(child, *f, (uint32_t)strlen(*f)); + if (!ts_node_is_null(rt)) { + def.return_type = cbm_node_text(a, rt, ctx->source); + break; + } + } + } + + // C++: trailing return type (auto method() -> Type) + if (def.return_type && strcmp(def.return_type, "auto") == 0 && + (ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA)) { + TSNode declarator = ts_node_child_by_field_name(child, "declarator", 10); + if (!ts_node_is_null(declarator)) { + uint32_t nc = ts_node_named_child_count(declarator); + for (uint32_t i = 0; i < nc; i++) { + TSNode ch = ts_node_named_child(declarator, i); + if (strcmp(ts_node_type(ch), "trailing_return_type") == 0) { + TSNode type_desc = ts_node_named_child_count(ch) > 0 + ? ts_node_named_child(ch, 0) : ch; + def.return_type = cbm_node_text(a, type_desc, ctx->source); + break; + } + } + } + } + def.decorators = extract_decorators(a, child, ctx->source, ctx->language, spec); def.docstring = extract_docstring(a, child, ctx->source, ctx->language); @@ -1985,6 +2104,94 @@ static void extract_variables(CBMExtractCtx* ctx, TSNode root, const CBMLangSpec } } +// Extract typed struct/class fields for cross-file LSP resolution (C/C++/CUDA/Go/Java/Rust etc.) +// Creates "Field" label definitions with return_type set to the field's type text. +// These are later collected by DefsToLSPDefs to build FieldDefs pipe-separated strings. +static void extract_class_fields(CBMExtractCtx* ctx, TSNode class_node, + const char* class_qn, const CBMLangSpec* spec) { + if (!spec->field_node_types || !spec->field_node_types[0]) return; + + TSNode body = find_class_body(class_node, ctx->language); + if (ts_node_is_null(body)) return; + + CBMArena* a = ctx->arena; + uint32_t count = ts_node_named_child_count(body); + for (uint32_t i = 0; i < count; i++) { + TSNode child = ts_node_named_child(body, i); + if (!cbm_kind_in_set(child, spec->field_node_types)) continue; + + // Skip function-pointer fields: walk the declarator chain — if any level + // is function_declarator, this is a function pointer, not a data field. + { + TSNode decl = ts_node_child_by_field_name(child, "declarator", 10); + bool is_func = false; + for (int depth = 0; depth < 5 && !ts_node_is_null(decl); depth++) { + const char* dk = ts_node_type(decl); + if (strcmp(dk, "function_declarator") == 0) { is_func = true; break; } + // Descend through parenthesized_declarator, pointer_declarator, etc. + TSNode inner = ts_node_child_by_field_name(decl, "declarator", 10); + if (ts_node_is_null(inner)) { + uint32_t nc = ts_node_named_child_count(decl); + for (uint32_t k = 0; k < nc; k++) { + inner = ts_node_named_child(decl, k); + if (!ts_node_is_null(inner)) break; + } + } + decl = inner; + } + if (is_func) continue; + } + + // Extract type from "type" field + TSNode type_node = ts_node_child_by_field_name(child, "type", 4); + if (ts_node_is_null(type_node)) continue; + char* type_text = cbm_node_text(a, type_node, ctx->source); + if (!type_text || !type_text[0]) continue; + + // Extract field name from "declarator" field (C/C++/CUDA/GLSL) + // or "name" field (Go, Java, Rust) + TSNode name_node = ts_node_child_by_field_name(child, "declarator", 10); + if (ts_node_is_null(name_node)) { + name_node = ts_node_child_by_field_name(child, "name", 4); + } + if (ts_node_is_null(name_node)) continue; + + // For C/C++: declarator might be a pointer_declarator or array_declarator wrapping field_identifier + const char* nk = ts_node_type(name_node); + if (strcmp(nk, "pointer_declarator") == 0 || strcmp(nk, "array_declarator") == 0) { + // Prepend pointer/array to type, get inner identifier + char* decl_text = cbm_node_text(a, name_node, ctx->source); + TSNode inner = ts_node_child_by_field_name(name_node, "declarator", 10); + if (!ts_node_is_null(inner)) { + name_node = inner; + nk = ts_node_type(name_node); + } else { + (void)decl_text; + continue; + } + } + + char* name = cbm_node_text(a, name_node, ctx->source); + if (!name || !name[0]) continue; + + const char* field_qn = cbm_arena_sprintf(a, "%s.%s", class_qn, name); + + CBMDefinition def; + memset(&def, 0, sizeof(def)); + def.name = name; + def.qualified_name = field_qn; + def.label = "Field"; + def.file_path = ctx->rel_path; + def.parent_class = class_qn; + def.return_type = type_text; + def.start_line = ts_node_start_point(child).row + 1; + def.end_line = ts_node_end_point(child).row + 1; + def.is_exported = cbm_is_exported(name, ctx->language); + + cbm_defs_push(&ctx->result->defs, a, def); + } +} + // Extract class-level variables (field declarations inside class bodies) static void extract_class_variables(CBMExtractCtx* ctx, TSNode class_node, const CBMLangSpec* spec) { if (!spec->variable_node_types || !spec->variable_node_types[0]) return; diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c new file mode 100644 index 000000000..2a2a69169 --- /dev/null +++ b/internal/cbm/lsp/c_lsp.c @@ -0,0 +1,3537 @@ +#include "c_lsp.h" +#include "../helpers.h" +#include +#include +#include +#include + +// Forward declarations +static void c_resolve_calls_in_node(CLSPContext* ctx, TSNode node); +static void c_emit_resolved_call(CLSPContext* ctx, const char* callee_qn, const char* strategy, float confidence); +static void c_emit_unresolved_call(CLSPContext* ctx, const char* expr_text, const char* reason); +static const CBMType* c_lookup_field_type(CLSPContext* ctx, const char* type_qn, const char* field_name, int depth); +static void c_process_function(CLSPContext* ctx, TSNode func_node); +static void c_process_namespace(CLSPContext* ctx, TSNode ns_node); +static void c_process_class(CLSPContext* ctx, TSNode class_node); +static void c_process_body_child(CLSPContext* ctx, TSNode child); +static const char* type_to_qn(const CBMType* t); +const CBMType* c_simplify_type(CLSPContext* ctx, const CBMType* t, bool unwrap_pointer); +const CBMType* c_eval_expr_type(CLSPContext* ctx, TSNode node); + +// External tree-sitter language functions (defined in grammar_*.c) +extern const TSLanguage* tree_sitter_c(void); +extern const TSLanguage* tree_sitter_cpp(void); + +// --- Smart pointer names for deref --- +static bool is_smart_ptr(const char* name) { + if (!name) return false; + return strcmp(name, "std.unique_ptr") == 0 || + strcmp(name, "std.shared_ptr") == 0 || + strcmp(name, "std.weak_ptr") == 0 || + strcmp(name, "std.auto_ptr") == 0 || + strcmp(name, "boost.shared_ptr") == 0 || + strcmp(name, "boost.scoped_ptr") == 0 || + // Legacy :: format (cross-file defs) + strcmp(name, "std::unique_ptr") == 0 || + strcmp(name, "std::shared_ptr") == 0 || + strcmp(name, "std::weak_ptr") == 0; +} + +// --- Helper: get node text --- +static char* c_node_text(CLSPContext* ctx, TSNode node) { + return cbm_node_text(ctx->arena, node, ctx->source); +} + +// --- Initialization --- + +void c_lsp_init(CLSPContext* ctx, CBMArena* arena, const char* source, int source_len, + const CBMTypeRegistry* registry, const char* module_qn, bool cpp_mode, + CBMResolvedCallArray* out) { + memset(ctx, 0, sizeof(CLSPContext)); + ctx->arena = arena; + ctx->source = source; + ctx->source_len = source_len; + ctx->registry = registry; + ctx->module_qn = module_qn; + ctx->cpp_mode = cpp_mode; + ctx->resolved_calls = out; + ctx->current_scope = cbm_scope_push(arena, NULL); + + const char* debug_env = getenv("CBM_LSP_DEBUG"); + ctx->debug = (debug_env && debug_env[0]); +} + +void c_lsp_add_include(CLSPContext* ctx, const char* header_path, const char* ns_qn) { + if (ctx->include_count % 32 == 0) { + int new_cap = ctx->include_count + 32; + const char** new_paths = (const char**)cbm_arena_alloc(ctx->arena, (new_cap + 1) * sizeof(const char*)); + const char** new_qns = (const char**)cbm_arena_alloc(ctx->arena, (new_cap + 1) * sizeof(const char*)); + if (!new_paths || !new_qns) return; + if (ctx->include_paths && ctx->include_count > 0) { + memcpy(new_paths, ctx->include_paths, ctx->include_count * sizeof(const char*)); + memcpy(new_qns, ctx->include_ns_qns, ctx->include_count * sizeof(const char*)); + } + ctx->include_paths = new_paths; + ctx->include_ns_qns = new_qns; + } + ctx->include_paths[ctx->include_count] = cbm_arena_strdup(ctx->arena, header_path); + ctx->include_ns_qns[ctx->include_count] = cbm_arena_strdup(ctx->arena, ns_qn); + ctx->include_count++; +} + +// --- Helper: add using namespace --- +static void c_add_using_namespace(CLSPContext* ctx, const char* ns_qn) { + if (ctx->using_ns_count >= ctx->using_ns_cap) { + int new_cap = ctx->using_ns_cap == 0 ? 16 : ctx->using_ns_cap * 2; + const char** new_arr = (const char**)cbm_arena_alloc(ctx->arena, new_cap * sizeof(const char*)); + if (!new_arr) return; + if (ctx->using_namespaces && ctx->using_ns_count > 0) + memcpy(new_arr, ctx->using_namespaces, ctx->using_ns_count * sizeof(const char*)); + ctx->using_namespaces = new_arr; + ctx->using_ns_cap = new_cap; + } + ctx->using_namespaces[ctx->using_ns_count++] = cbm_arena_strdup(ctx->arena, ns_qn); +} + +// --- Helper: add using declaration --- +static void c_add_using_decl(CLSPContext* ctx, const char* short_name, const char* full_qn) { + if (ctx->using_decl_count >= ctx->using_decl_cap) { + int new_cap = ctx->using_decl_cap == 0 ? 16 : ctx->using_decl_cap * 2; + const char** new_names = (const char**)cbm_arena_alloc(ctx->arena, new_cap * sizeof(const char*)); + const char** new_qns = (const char**)cbm_arena_alloc(ctx->arena, new_cap * sizeof(const char*)); + if (!new_names || !new_qns) return; + if (ctx->using_decl_names && ctx->using_decl_count > 0) { + memcpy(new_names, ctx->using_decl_names, ctx->using_decl_count * sizeof(const char*)); + memcpy(new_qns, ctx->using_decl_qns, ctx->using_decl_count * sizeof(const char*)); + } + ctx->using_decl_names = new_names; + ctx->using_decl_qns = new_qns; + ctx->using_decl_cap = new_cap; + } + ctx->using_decl_names[ctx->using_decl_count] = cbm_arena_strdup(ctx->arena, short_name); + ctx->using_decl_qns[ctx->using_decl_count] = cbm_arena_strdup(ctx->arena, full_qn); + ctx->using_decl_count++; +} + +// --- Helper: add namespace alias --- +static void c_add_ns_alias(CLSPContext* ctx, const char* alias, const char* full_qn) { + if (ctx->ns_alias_count >= ctx->ns_alias_cap) { + int new_cap = ctx->ns_alias_cap == 0 ? 8 : ctx->ns_alias_cap * 2; + const char** new_names = (const char**)cbm_arena_alloc(ctx->arena, new_cap * sizeof(const char*)); + const char** new_qns = (const char**)cbm_arena_alloc(ctx->arena, new_cap * sizeof(const char*)); + if (!new_names || !new_qns) return; + if (ctx->ns_alias_names && ctx->ns_alias_count > 0) { + memcpy(new_names, ctx->ns_alias_names, ctx->ns_alias_count * sizeof(const char*)); + memcpy(new_qns, ctx->ns_alias_qns, ctx->ns_alias_count * sizeof(const char*)); + } + ctx->ns_alias_names = new_names; + ctx->ns_alias_qns = new_qns; + ctx->ns_alias_cap = new_cap; + } + ctx->ns_alias_names[ctx->ns_alias_count] = cbm_arena_strdup(ctx->arena, alias); + ctx->ns_alias_qns[ctx->ns_alias_count] = cbm_arena_strdup(ctx->arena, full_qn); + ctx->ns_alias_count++; +} + +// --- Helper: track function pointer target --- +static void c_add_fp_target(CLSPContext* ctx, const char* var_name, const char* target_qn) { + if (ctx->fp_count >= ctx->fp_cap) { + int new_cap = ctx->fp_cap == 0 ? 16 : ctx->fp_cap * 2; + const char** new_names = (const char**)cbm_arena_alloc(ctx->arena, new_cap * sizeof(const char*)); + const char** new_qns = (const char**)cbm_arena_alloc(ctx->arena, new_cap * sizeof(const char*)); + if (!new_names || !new_qns) return; + if (ctx->fp_var_names && ctx->fp_count > 0) { + memcpy(new_names, ctx->fp_var_names, ctx->fp_count * sizeof(const char*)); + memcpy(new_qns, ctx->fp_target_qns, ctx->fp_count * sizeof(const char*)); + } + ctx->fp_var_names = new_names; + ctx->fp_target_qns = new_qns; + ctx->fp_cap = new_cap; + } + ctx->fp_var_names[ctx->fp_count] = cbm_arena_strdup(ctx->arena, var_name); + ctx->fp_target_qns[ctx->fp_count] = cbm_arena_strdup(ctx->arena, target_qn); + ctx->fp_count++; +} + +// Look up function pointer target +static const char* c_lookup_fp_target(CLSPContext* ctx, const char* var_name) { + for (int i = ctx->fp_count - 1; i >= 0; i--) { + if (strcmp(ctx->fp_var_names[i], var_name) == 0) + return ctx->fp_target_qns[i]; + } + return NULL; +} + +// --- Helper: pending template calls (member calls on TYPE_PARAM inside templates) --- +static void c_add_pending_template_call(CLSPContext* ctx, const char* func_qn, + const char* type_param, const char* method_name, int arg_count) { + if (!func_qn || !type_param || !method_name) return; + if (ctx->pending_tc_count >= ctx->pending_tc_cap) { + int new_cap = ctx->pending_tc_cap == 0 ? 16 : ctx->pending_tc_cap * 2; + size_t sz = new_cap * sizeof(ctx->pending_template_calls[0]); + void* new_arr = cbm_arena_alloc(ctx->arena, sz); + if (!new_arr) return; + if (ctx->pending_template_calls && ctx->pending_tc_count > 0) + memcpy(new_arr, ctx->pending_template_calls, + ctx->pending_tc_count * sizeof(ctx->pending_template_calls[0])); + ctx->pending_template_calls = new_arr; + ctx->pending_tc_cap = new_cap; + } + int i = ctx->pending_tc_count++; + ctx->pending_template_calls[i].func_qn = cbm_arena_strdup(ctx->arena, func_qn); + ctx->pending_template_calls[i].type_param = cbm_arena_strdup(ctx->arena, type_param); + ctx->pending_template_calls[i].method_name = cbm_arena_strdup(ctx->arena, method_name); + ctx->pending_template_calls[i].arg_count = arg_count; +} + +// Resolve pending template calls for a function being called with known arg types. +// Deduces type params from call-site args and resolves stored method calls. +static void c_resolve_pending_template_calls(CLSPContext* ctx, + const CBMRegisteredFunc* callee, const CBMType** call_arg_types, int call_arg_count) { + if (!callee || !callee->type_param_names || !call_arg_types) return; + + // Build type param → concrete type mapping from call-site arguments + const char** tpn = callee->type_param_names; + const CBMType* param_map[8] = {0}; + int tpn_count = 0; + while (tpn[tpn_count] && tpn_count < 8) tpn_count++; + + // Match call arg types against function param types to deduce type params + if (callee->signature && callee->signature->kind == CBM_TYPE_FUNC && + callee->signature->data.func.param_types) { + for (int i = 0; i < call_arg_count; i++) { + const CBMType* formal = callee->signature->data.func.param_types[i]; + if (!formal || !call_arg_types[i]) continue; + // Unwrap references/pointers + while (formal && (formal->kind == CBM_TYPE_REFERENCE || + formal->kind == CBM_TYPE_RVALUE_REF || + formal->kind == CBM_TYPE_POINTER)) { + formal = (formal->kind == CBM_TYPE_POINTER) ? + formal->data.pointer.elem : formal->data.reference.elem; + } + if (formal && formal->kind == CBM_TYPE_TYPE_PARAM) { + for (int j = 0; j < tpn_count; j++) { + if (strcmp(tpn[j], formal->data.type_param.name) == 0) { + const CBMType* arg = call_arg_types[i]; + arg = c_simplify_type(ctx, arg, false); + param_map[j] = arg; + } + } + } + } + } + + // Resolve pending calls for this function + for (int i = 0; i < ctx->pending_tc_count; i++) { + if (strcmp(ctx->pending_template_calls[i].func_qn, callee->qualified_name) != 0) + continue; + const char* tp = ctx->pending_template_calls[i].type_param; + const char* method = ctx->pending_template_calls[i].method_name; + + // Find which type param this is + for (int j = 0; j < tpn_count; j++) { + if (strcmp(tpn[j], tp) != 0 || !param_map[j]) continue; + const char* concrete_qn = type_to_qn(param_map[j]); + if (!concrete_qn) continue; + // Look up the method on the concrete type + const CBMRegisteredFunc* m = c_lookup_member(ctx, concrete_qn, method); + if (m) { + c_emit_resolved_call(ctx, m->qualified_name, "lsp_template_instantiation", 0.90f); + } + break; + } + } +} + +// --- Helper: extract call argument types for overload scoring --- +static const CBMType** c_extract_call_arg_types(CLSPContext* ctx, TSNode call_node, int* out_count) { + TSNode args = ts_node_child_by_field_name(call_node, "arguments", 9); + if (ts_node_is_null(args)) { *out_count = 0; return NULL; } + uint32_t nc = ts_node_named_child_count(args); + if (nc == 0) { *out_count = 0; return NULL; } + const CBMType** types = (const CBMType**)cbm_arena_alloc(ctx->arena, (nc + 1) * sizeof(const CBMType*)); + if (!types) { *out_count = 0; return NULL; } + int count = 0; + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_named_child(args, i); + if (!ts_node_is_null(child)) { + types[count++] = c_eval_expr_type(ctx, child); + } + } + types[count] = NULL; + *out_count = count; + return types; +} + +// --- Helper: parse template parameter defaults --- +static void c_parse_template_params(CLSPContext* ctx, TSNode template_decl) { + TSNode params = ts_node_child_by_field_name(template_decl, "parameters", 10); + if (ts_node_is_null(params)) return; + + ctx->template_param_count = 0; + uint32_t nc = ts_node_named_child_count(params); + if (nc == 0) return; + + const char** names = (const char**)cbm_arena_alloc(ctx->arena, (nc + 1) * sizeof(const char*)); + const CBMType** defaults = (const CBMType**)cbm_arena_alloc(ctx->arena, (nc + 1) * sizeof(const CBMType*)); + if (!names || !defaults) return; + + int idx = 0; + for (uint32_t i = 0; i < nc && idx < 16; i++) { + TSNode param = ts_node_named_child(params, i); + if (ts_node_is_null(param)) continue; + const char* pk = ts_node_type(param); + + // type_parameter_declaration: template + // optional_type_parameter_declaration: template + if (strcmp(pk, "type_parameter_declaration") == 0 || + strcmp(pk, "optional_type_parameter_declaration") == 0) { + // Get parameter name + TSNode name_node = ts_node_child_by_field_name(param, "name", 4); + if (ts_node_is_null(name_node)) { + // Some grammars put the identifier as a direct child + for (uint32_t j = 0; j < ts_node_named_child_count(param); j++) { + TSNode ch = ts_node_named_child(param, j); + if (strcmp(ts_node_type(ch), "type_identifier") == 0 || + strcmp(ts_node_type(ch), "identifier") == 0) { + name_node = ch; + break; + } + } + } + + char* pname = NULL; + if (!ts_node_is_null(name_node)) pname = c_node_text(ctx, name_node); + if (!pname) pname = cbm_arena_sprintf(ctx->arena, "T%d", idx); + + names[idx] = pname; + + // Get default type + TSNode default_node = ts_node_child_by_field_name(param, "default_type", 12); + if (ts_node_is_null(default_node)) { + // Try "default_value" field + default_node = ts_node_child_by_field_name(param, "default", 7); + } + if (!ts_node_is_null(default_node)) { + defaults[idx] = c_parse_type_node(ctx, default_node); + } else { + defaults[idx] = NULL; + } + idx++; + } + } + + names[idx] = NULL; + defaults[idx] = NULL; + ctx->template_param_names = names; + ctx->template_param_defaults = defaults; + ctx->template_param_count = idx; +} + +// Resolve a template type parameter using defaults from current template scope +static const CBMType* c_resolve_template_param(CLSPContext* ctx, const char* param_name) { + if (!param_name || !ctx->template_param_names) return NULL; + for (int i = 0; i < ctx->template_param_count; i++) { + if (ctx->template_param_names[i] && + strcmp(ctx->template_param_names[i], param_name) == 0) { + return ctx->template_param_defaults[i]; // may be NULL if no default + } + } + return NULL; +} + +// --- Helper: resolve namespace alias --- +static const char* c_resolve_ns_alias(CLSPContext* ctx, const char* name) { + for (int i = 0; i < ctx->ns_alias_count; i++) { + if (strcmp(ctx->ns_alias_names[i], name) == 0) + return ctx->ns_alias_qns[i]; + } + return NULL; +} + +// --- C/C++ builtin check --- +static bool is_c_builtin_type(const char* name) { + static const char* builtins[] = { + "int", "char", "short", "long", "float", "double", "void", + "bool", "size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t", + "int8_t", "int16_t", "int32_t", "int64_t", + "uint8_t", "uint16_t", "uint32_t", "uint64_t", + "wchar_t", "char16_t", "char32_t", "char8_t", + "unsigned", "signed", + NULL + }; + for (const char** b = builtins; *b; b++) { + if (strcmp(name, *b) == 0) return true; + } + return false; +} + +static bool is_c_builtin_func(const char* name) { + // C stdlib functions are registered in the registry, not hardcoded here. + // But we skip certain compiler builtins that should not generate CALLS edges. + static const char* skip[] = { + "__builtin_expect", "__builtin_unreachable", "__builtin_offsetof", + "__builtin_va_start", "__builtin_va_end", "__builtin_va_arg", + "sizeof", "alignof", "_Alignof", "typeof", "decltype", + "static_assert", "_Static_assert", + NULL + }; + for (const char** b = skip; *b; b++) { + if (strcmp(name, *b) == 0) return true; + } + return false; +} + +// --- Qualified name construction --- + +// Build a qualified name from a namespace-qualified identifier. +// Converts "::" to "." for our QN format. +// Inline ABI namespaces to strip during QN construction. +// Only ABI-internal segments, NOT __detail (contains internal types). +static bool is_inline_abi_ns(const char* seg, size_t seg_len) { + return (seg_len == 3 && memcmp(seg, "__1", 3) == 0) || + (seg_len == 7 && memcmp(seg, "__cxx11", 7) == 0) || + (seg_len == 9 && memcmp(seg, "__gnu_cxx", 9) == 0); +} + +static const char* c_build_qn(CLSPContext* ctx, const char* text) { + if (!text) return NULL; + // Replace :: with . and strip inline ABI namespaces + size_t len = strlen(text); + char* buf = (char*)cbm_arena_alloc(ctx->arena, len + 1); + if (!buf) return text; + size_t j = 0; + for (size_t i = 0; i < len; i++) { + if (text[i] == ':' && i + 1 < len && text[i + 1] == ':') { + // Peek at next segment to check for inline ABI namespace + size_t seg_start = i + 2; + size_t seg_end = seg_start; + while (seg_end < len && text[seg_end] != ':') seg_end++; + size_t seg_len = seg_end - seg_start; + if (seg_len > 0 && is_inline_abi_ns(text + seg_start, seg_len) && + seg_end + 1 < len && text[seg_end] == ':' && text[seg_end + 1] == ':') { + // Skip ABI ns + its trailing :: + i = seg_end + 1; // loop increments to seg_end+2 = after "::" + // Don't emit a dot — we're removing this segment + continue; + } + buf[j++] = '.'; + i++; // skip second ':' + } else { + buf[j++] = text[i]; + } + } + buf[j] = '\0'; + return buf; +} + +// --- Name lookup (C++ multi-scope resolution) --- + +// Look up a name following C++ name resolution order: +// 1. Current scope chain +// 2. Using declarations +// 3. Enclosing class (implicit this) +// 4. Using namespaces +// 5. Current namespace +// 6. Module scope +static const char* c_resolve_name(CLSPContext* ctx, const char* name) { + if (!name) return NULL; + + // 1. Scope lookup + const CBMType* t = cbm_scope_lookup(ctx->current_scope, name); + if (!cbm_type_is_unknown(t)) return NULL; // found in scope, not a function + + // 2. Using declarations + for (int i = 0; i < ctx->using_decl_count; i++) { + if (strcmp(ctx->using_decl_names[i], name) == 0) + return ctx->using_decl_qns[i]; + } + + // 3. Enclosing class (implicit this) + if (ctx->enclosing_class_qn) { + const CBMRegisteredFunc* f = cbm_registry_lookup_method(ctx->registry, + ctx->enclosing_class_qn, name); + if (f) return f->qualified_name; + } + + // 4. Using namespaces + for (int i = 0; i < ctx->using_ns_count; i++) { + const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, + ctx->using_namespaces[i], name); + if (f) return f->qualified_name; + } + + // 5. Current namespace + parent namespaces + if (ctx->current_namespace) { + const char* ns = ctx->current_namespace; + while (ns && ns[0]) { + const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, ns, name); + if (f) return f->qualified_name; + // Walk up: "a.b.c" -> "a.b" -> "a" + const char* dot = strrchr(ns, '.'); + if (!dot) break; + char* parent = (char*)cbm_arena_alloc(ctx->arena, (size_t)(dot - ns) + 1); + if (!parent) break; + memcpy(parent, ns, (size_t)(dot - ns)); + parent[dot - ns] = '\0'; + ns = parent; + } + } + + // 6. Module scope + if (ctx->module_qn) { + const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, + ctx->module_qn, name); + if (f) return f->qualified_name; + } + + // 7. Global scope (no prefix — C stdlib functions) + const CBMRegisteredFunc* f = cbm_registry_lookup_func(ctx->registry, name); + if (f) return f->qualified_name; + + return NULL; +} + +// --- ADL (Argument-Dependent Lookup) --- +// Extract the namespace from a qualified name: "std.vector" → "std", "boost.asio.ip" → "boost.asio" +static const char* extract_namespace_from_qn(CBMArena* arena, const char* qn) { + if (!qn) return NULL; + const char* dot = strrchr(qn, '.'); + if (!dot || dot == qn) return NULL; + size_t len = (size_t)(dot - qn); + char* ns = (char*)cbm_arena_alloc(arena, len + 1); + if (!ns) return NULL; + memcpy(ns, qn, len); + ns[len] = '\0'; + return ns; +} + +// ADL: resolve unqualified function call by searching namespaces of argument types. +// E.g., swap(a, b) where a is std::string → look up std::swap. +static const char* c_adl_resolve(CLSPContext* ctx, const char* name, TSNode call_node) { + if (!ctx->cpp_mode || !name) return NULL; + + TSNode args = ts_node_child_by_field_name(call_node, "arguments", 9); + if (ts_node_is_null(args)) return NULL; + + uint32_t nc = ts_node_named_child_count(args); + if (nc == 0) return NULL; + + // Collect unique namespace QNs from argument types (max 8 to bound work) + const char* namespaces[8]; + int ns_count = 0; + + for (uint32_t i = 0; i < nc && ns_count < 8; i++) { + TSNode arg = ts_node_named_child(args, i); + if (ts_node_is_null(arg)) continue; + + const CBMType* arg_type = c_eval_expr_type(ctx, arg); + const char* qn = type_to_qn(c_simplify_type(ctx, arg_type, false)); + if (!qn) continue; + + const char* ns = extract_namespace_from_qn(ctx->arena, qn); + if (!ns) continue; + + // Deduplicate + bool dup = false; + for (int j = 0; j < ns_count; j++) { + if (strcmp(namespaces[j], ns) == 0) { dup = true; break; } + } + if (!dup) namespaces[ns_count++] = ns; + } + + // Try each namespace + for (int i = 0; i < ns_count; i++) { + const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, + namespaces[i], name); + if (f) return f->qualified_name; + } + + return NULL; +} + +// Resolve a name to a function QN via registry only (skips scope check). +// Used for function pointer target resolution where the name IS in scope +// as a function definition but we need the registry QN, not NULL. +static const char* c_resolve_name_to_func_qn(CLSPContext* ctx, const char* name) { + if (!name) return NULL; + + // Using declarations + for (int i = 0; i < ctx->using_decl_count; i++) { + if (strcmp(ctx->using_decl_names[i], name) == 0) + return ctx->using_decl_qns[i]; + } + + // Enclosing class + if (ctx->enclosing_class_qn) { + const CBMRegisteredFunc* f = cbm_registry_lookup_method(ctx->registry, + ctx->enclosing_class_qn, name); + if (f) return f->qualified_name; + } + + // Using namespaces + for (int i = 0; i < ctx->using_ns_count; i++) { + const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, + ctx->using_namespaces[i], name); + if (f) return f->qualified_name; + } + + // Current namespace + parents + if (ctx->current_namespace) { + const char* ns = ctx->current_namespace; + while (ns && ns[0]) { + const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, ns, name); + if (f) return f->qualified_name; + const char* dot = strrchr(ns, '.'); + if (!dot) break; + char* parent = (char*)cbm_arena_alloc(ctx->arena, (size_t)(dot - ns) + 1); + if (!parent) break; + memcpy(parent, ns, (size_t)(dot - ns)); + parent[dot - ns] = '\0'; + ns = parent; + } + } + + // Module scope + if (ctx->module_qn) { + const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, + ctx->module_qn, name); + if (f) return f->qualified_name; + } + + // Global scope + const CBMRegisteredFunc* gf = cbm_registry_lookup_func(ctx->registry, name); + if (gf) return gf->qualified_name; + + return NULL; +} + +// Resolve a name to a type (for identifiers used as types) +static const CBMType* c_resolve_name_to_type(CLSPContext* ctx, const char* name) { + if (!name) return cbm_type_unknown(); + + // Builtin types + if (is_c_builtin_type(name)) return cbm_type_builtin(ctx->arena, name); + + // Template type parameters (T, U, etc.) + if (ctx->in_template && ctx->template_param_names) { + for (int i = 0; i < ctx->template_param_count; i++) { + if (ctx->template_param_names[i] && + strcmp(ctx->template_param_names[i], name) == 0) { + // Return default if available, otherwise TYPE_PARAM + if (ctx->template_param_defaults && ctx->template_param_defaults[i]) + return ctx->template_param_defaults[i]; + return cbm_type_type_param(ctx->arena, name); + } + } + } + + // Using declarations + for (int i = 0; i < ctx->using_decl_count; i++) { + if (strcmp(ctx->using_decl_names[i], name) == 0) { + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, ctx->using_decl_qns[i]); + if (rt) return cbm_type_named(ctx->arena, rt->qualified_name); + } + } + + // Enclosing class (nested types: Factory::Product) + if (ctx->enclosing_class_qn) { + const char* qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->enclosing_class_qn, name); + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, qn); + if (rt) return cbm_type_named(ctx->arena, qn); + } + + // Current namespace + if (ctx->current_namespace) { + const char* qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->current_namespace, name); + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, qn); + if (rt) return cbm_type_named(ctx->arena, qn); + } + + // Module scope + if (ctx->module_qn) { + const char* qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, name); + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, qn); + if (rt) return cbm_type_named(ctx->arena, qn); + } + + // Using namespaces + for (int i = 0; i < ctx->using_ns_count; i++) { + const char* qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->using_namespaces[i], name); + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, qn); + if (rt) return cbm_type_named(ctx->arena, qn); + } + + // Try std:: prefix (very common) + if (ctx->cpp_mode) { + const char* std_qn = cbm_arena_sprintf(ctx->arena, "std.%s", name); + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, std_qn); + if (rt) return cbm_type_named(ctx->arena, std_qn); + } + + // Unresolved — return named with module prefix + if (ctx->module_qn) { + return cbm_type_named(ctx->arena, + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, name)); + } + return cbm_type_named(ctx->arena, name); +} + +// ============================================================================ +// c_parse_type_node: AST type node -> CBMType +// ============================================================================ + +const CBMType* c_parse_type_node(CLSPContext* ctx, TSNode node) { + if (ts_node_is_null(node)) return cbm_type_unknown(); + const char* kind = ts_node_type(node); + + // primitive_type: int, double, char, void, bool + if (strcmp(kind, "primitive_type") == 0) { + char* name = c_node_text(ctx, node); + return name ? cbm_type_builtin(ctx->arena, name) : cbm_type_unknown(); + } + + // sized_type_specifier: unsigned int, long long, etc. + if (strcmp(kind, "sized_type_specifier") == 0) { + char* name = c_node_text(ctx, node); + return name ? cbm_type_builtin(ctx->arena, name) : cbm_type_unknown(); + } + + // type_identifier: Foo, MyClass + if (strcmp(kind, "type_identifier") == 0) { + char* name = c_node_text(ctx, node); + if (!name) return cbm_type_unknown(); + return c_resolve_name_to_type(ctx, name); + } + + // qualified_identifier: std::vector, ns::Foo + if (strcmp(kind, "qualified_identifier") == 0 || + strcmp(kind, "scoped_type_identifier") == 0) { + // Check if name field is a template_type (e.g., std::vector) + // If so, build scoped QN prefix and delegate to template_type handler + TSNode name_child = ts_node_child_by_field_name(node, "name", 4); + if (!ts_node_is_null(name_child) && + strcmp(ts_node_type(name_child), "template_type") == 0) { + // Build scope prefix from scope chain + TSNode scope_node = ts_node_child_by_field_name(node, "scope", 5); + char* scope_text = NULL; + if (!ts_node_is_null(scope_node)) { + scope_text = c_node_text(ctx, scope_node); + if (scope_text) { + const char* alias_qn = c_resolve_ns_alias(ctx, scope_text); + if (alias_qn) scope_text = (char*)alias_qn; + } + } + // Parse the template_type's own name + TSNode tmpl_name = ts_node_child_by_field_name(name_child, "name", 4); + TSNode tmpl_args = ts_node_child_by_field_name(name_child, "arguments", 9); + char* tmpl_name_text = NULL; + if (!ts_node_is_null(tmpl_name)) tmpl_name_text = c_node_text(ctx, tmpl_name); + if (!tmpl_name_text) return cbm_type_unknown(); + + // Build full QN: scope.template_name + const char* template_qn; + if (scope_text) { + const char* scoped = cbm_arena_sprintf(ctx->arena, "%s::%s", scope_text, tmpl_name_text); + template_qn = c_build_qn(ctx, scoped); + } else { + template_qn = c_build_qn(ctx, tmpl_name_text); + } + + // Resolve against registry + const CBMRegisteredType* trt = cbm_registry_lookup_type(ctx->registry, template_qn); + if (!trt && ctx->module_qn) { + const char* mod_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, template_qn); + trt = cbm_registry_lookup_type(ctx->registry, mod_qn); + if (trt) template_qn = mod_qn; + } + + // Parse template arguments + const CBMType* targs[16]; + int targ_count = 0; + if (!ts_node_is_null(tmpl_args)) { + uint32_t nc = ts_node_named_child_count(tmpl_args); + for (uint32_t i = 0; i < nc && targ_count < 15; i++) { + TSNode arg = ts_node_named_child(tmpl_args, i); + if (ts_node_is_null(arg)) continue; + const char* ak = ts_node_type(arg); + if (strcmp(ak, "type_descriptor") == 0) { + if (ts_node_named_child_count(arg) > 0) + targs[targ_count++] = c_parse_type_node(ctx, ts_node_named_child(arg, 0)); + } else { + targs[targ_count++] = c_parse_type_node(ctx, arg); + } + } + } + + if (targ_count > 0) + return cbm_type_template(ctx->arena, template_qn, targs, targ_count); + return cbm_type_named(ctx->arena, template_qn); + } + + char* text = c_node_text(ctx, node); + if (!text) return cbm_type_unknown(); + + // Check for namespace alias at start + TSNode scope_node = ts_node_child_by_field_name(node, "scope", 5); + if (!ts_node_is_null(scope_node)) { + char* scope_text = c_node_text(ctx, scope_node); + if (scope_text) { + const char* alias_qn = c_resolve_ns_alias(ctx, scope_text); + if (alias_qn) { + if (!ts_node_is_null(name_child)) { + char* name = c_node_text(ctx, name_child); + if (name) text = (char*)cbm_arena_sprintf(ctx->arena, "%s::%s", alias_qn, name); + } + } + } + } + + const char* qn = c_build_qn(ctx, text); + // Check registry + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, qn); + if (rt) return cbm_type_named(ctx->arena, qn); + return cbm_type_named(ctx->arena, qn); + } + + // template_type: vector, map + if (strcmp(kind, "template_type") == 0) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + TSNode args_node = ts_node_child_by_field_name(node, "arguments", 9); + + char* name_text = NULL; + if (!ts_node_is_null(name_node)) { + name_text = c_node_text(ctx, name_node); + } + if (!name_text) return cbm_type_unknown(); + + const char* template_qn = c_build_qn(ctx, name_text); + + // Try resolving short name to full QN + const CBMRegisteredType* trt = cbm_registry_lookup_type(ctx->registry, template_qn); + if (!trt && ctx->module_qn) { + // Try with module prefix + const char* mod_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, template_qn); + trt = cbm_registry_lookup_type(ctx->registry, mod_qn); + if (trt) template_qn = mod_qn; + } + if (!trt) { + // Try with std:: prefix + const char* std_qn = cbm_arena_sprintf(ctx->arena, "std.%s", template_qn); + trt = cbm_registry_lookup_type(ctx->registry, std_qn); + if (trt) template_qn = std_qn; + } + + // Parse template arguments + const CBMType* targs[16]; + int targ_count = 0; + if (!ts_node_is_null(args_node)) { + uint32_t nc = ts_node_named_child_count(args_node); + for (uint32_t i = 0; i < nc && targ_count < 15; i++) { + TSNode arg = ts_node_named_child(args_node, i); + if (ts_node_is_null(arg)) continue; + const char* ak = ts_node_type(arg); + if (strcmp(ak, "type_descriptor") == 0) { + // type_descriptor wraps a type node + if (ts_node_named_child_count(arg) > 0) { + targs[targ_count++] = c_parse_type_node(ctx, ts_node_named_child(arg, 0)); + } + } else { + targs[targ_count++] = c_parse_type_node(ctx, arg); + } + } + } + + if (targ_count > 0) { + return cbm_type_template(ctx->arena, template_qn, targs, targ_count); + } + return cbm_type_named(ctx->arena, template_qn); + } + + // pointer_declarator / pointer (*) in type context + if (strcmp(kind, "pointer_declarator") == 0 || + strcmp(kind, "pointer_type") == 0) { + if (ts_node_named_child_count(node) > 0) { + return cbm_type_pointer(ctx->arena, + c_parse_type_node(ctx, ts_node_named_child(node, 0))); + } + return cbm_type_pointer(ctx->arena, cbm_type_unknown()); + } + + // reference_declarator / reference_type (&) + if (strcmp(kind, "reference_declarator") == 0 || + strcmp(kind, "abstract_reference_declarator") == 0) { + if (ts_node_named_child_count(node) > 0) { + return cbm_type_reference(ctx->arena, + c_parse_type_node(ctx, ts_node_named_child(node, 0))); + } + return cbm_type_reference(ctx->arena, cbm_type_unknown()); + } + + // auto keyword + if (strcmp(kind, "auto") == 0 || strcmp(kind, "placeholder_type_specifier") == 0) { + return cbm_type_unknown(); // will be resolved from initializer + } + + // decltype + if (strcmp(kind, "decltype") == 0) { + if (ts_node_named_child_count(node) > 0) { + return c_eval_expr_type(ctx, ts_node_named_child(node, 0)); + } + return cbm_type_unknown(); + } + + // const/volatile qualifier — strip and recurse + if (strcmp(kind, "type_qualifier") == 0) { + return cbm_type_unknown(); // qualifier itself has no type + } + + // array_declarator: int[10] + if (strcmp(kind, "array_declarator") == 0 || strcmp(kind, "array_type") == 0) { + if (ts_node_named_child_count(node) > 0) { + return cbm_type_slice(ctx->arena, + c_parse_type_node(ctx, ts_node_named_child(node, 0))); + } + return cbm_type_unknown(); + } + + // function_declarator: int (*)(int) + if (strcmp(kind, "function_declarator") == 0 || + strcmp(kind, "abstract_function_declarator") == 0) { + return cbm_type_func(ctx->arena, NULL, NULL, NULL); + } + + // struct_specifier / class_specifier (inline struct/class type) + if (strcmp(kind, "struct_specifier") == 0 || strcmp(kind, "class_specifier") == 0) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (!ts_node_is_null(name_node)) { + char* name = c_node_text(ctx, name_node); + if (name) return c_resolve_name_to_type(ctx, name); + } + // Anonymous struct + CBMType* t = (CBMType*)cbm_arena_alloc(ctx->arena, sizeof(CBMType)); + memset(t, 0, sizeof(CBMType)); + t->kind = CBM_TYPE_STRUCT; + return t; + } + + // enum_specifier + if (strcmp(kind, "enum_specifier") == 0) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (!ts_node_is_null(name_node)) { + char* name = c_node_text(ctx, name_node); + if (name) return c_resolve_name_to_type(ctx, name); + } + return cbm_type_builtin(ctx->arena, "int"); + } + + // type_descriptor: wraps a type with optional qualifiers + if (strcmp(kind, "type_descriptor") == 0) { + uint32_t nc = ts_node_named_child_count(node); + // Find the actual type node (skip qualifiers) + 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, "type_qualifier") != 0) { + return c_parse_type_node(ctx, child); + } + } + return cbm_type_unknown(); + } + + // parenthesized_type: (T) + if (strcmp(kind, "parenthesized_type") == 0 && ts_node_named_child_count(node) > 0) { + return c_parse_type_node(ctx, ts_node_named_child(node, 0)); + } + + // dependent_type: in template scope + if (strcmp(kind, "dependent_type") == 0 || strcmp(kind, "dependent_name") == 0) { + char* text = c_node_text(ctx, node); + if (text) return cbm_type_named(ctx->arena, c_build_qn(ctx, text)); + return cbm_type_unknown(); + } + + return cbm_type_unknown(); +} + +// ============================================================================ +// c_simplify_type: multi-step type unwrapping (clangd simplifyType) +// ============================================================================ + +const CBMType* c_simplify_type(CLSPContext* ctx, const CBMType* t, bool unwrap_pointer) { + for (int i = 0; i < 64 && t; i++) { + // Resolve aliases + if (t->kind == CBM_TYPE_ALIAS) { + if (t->data.alias.underlying) { + t = t->data.alias.underlying; + continue; + } + break; + } + + // Resolve template type parameters using defaults + if (t->kind == CBM_TYPE_TYPE_PARAM && ctx->in_template) { + const CBMType* resolved = c_resolve_template_param(ctx, t->data.type_param.name); + if (resolved) { + t = resolved; + continue; + } + break; // no default available + } + + // Unwrap references + if (t->kind == CBM_TYPE_REFERENCE || t->kind == CBM_TYPE_RVALUE_REF) { + t = t->data.reference.elem; + continue; + } + + if (!unwrap_pointer) break; + + // Unwrap pointer + if (t->kind == CBM_TYPE_POINTER) { + t = t->data.pointer.elem; + break; + } + + // Smart pointer: extract first template arg + if (t->kind == CBM_TYPE_TEMPLATE && is_smart_ptr(t->data.template_type.template_name)) { + if (t->data.template_type.arg_count > 0 && t->data.template_type.template_args) { + t = t->data.template_type.template_args[0]; + break; + } + } + + // Named type with operator-> + if (t->kind == CBM_TYPE_NAMED) { + const CBMRegisteredFunc* op = cbm_registry_lookup_method(ctx->registry, + t->data.named.qualified_name, "operator->"); + if (op && op->signature && op->signature->kind == CBM_TYPE_FUNC && + op->signature->data.func.return_types && op->signature->data.func.return_types[0]) { + t = op->signature->data.func.return_types[0]; + // deref the pointer return + if (t->kind == CBM_TYPE_POINTER) t = t->data.pointer.elem; + break; + } + } + + break; + } + return t ? t : cbm_type_unknown(); +} + +// Get the QN string from a type (for member lookup) +static const char* type_to_qn(const CBMType* t) { + if (!t) return NULL; + switch (t->kind) { + case CBM_TYPE_NAMED: return t->data.named.qualified_name; + case CBM_TYPE_TEMPLATE: return t->data.template_type.template_name; + default: return NULL; + } +} + +// ============================================================================ +// c_eval_expr_type: recursive expression type evaluator +// ============================================================================ + +const CBMType* c_eval_expr_type(CLSPContext* ctx, TSNode node) { + if (ts_node_is_null(node)) return cbm_type_unknown(); + const char* kind = ts_node_type(node); + + // --- identifier: scope lookup --- + if (strcmp(kind, "identifier") == 0) { + char* name = c_node_text(ctx, node); + if (!name) return cbm_type_unknown(); + + // Scope lookup + const CBMType* t = cbm_scope_lookup(ctx->current_scope, name); + if (!cbm_type_is_unknown(t)) return t; + + // Check if it's a type name (for constructor calls / casts) + const CBMType* type = c_resolve_name_to_type(ctx, name); + if (type && type->kind == CBM_TYPE_NAMED) { + // Could be a constructor call — return as type + return type; + } + + // Check if it's a registered function + const char* fqn = c_resolve_name(ctx, name); + if (fqn) { + const CBMRegisteredFunc* f = cbm_registry_lookup_func(ctx->registry, fqn); + if (f && f->signature) return f->signature; + } + + return cbm_type_unknown(); + } + + // --- this --- + if (strcmp(kind, "this") == 0) { + if (ctx->enclosing_class_qn) { + return cbm_type_pointer(ctx->arena, + cbm_type_named(ctx->arena, ctx->enclosing_class_qn)); + } + return cbm_type_unknown(); + } + + // --- field_expression: a.b or a->b --- + if (strcmp(kind, "field_expression") == 0) { + TSNode arg_node = ts_node_child_by_field_name(node, "argument", 8); + TSNode field_node = ts_node_child_by_field_name(node, "field", 5); + if (ts_node_is_null(arg_node) || ts_node_is_null(field_node)) + return cbm_type_unknown(); + + char* field_name = c_node_text(ctx, field_node); + if (!field_name) return cbm_type_unknown(); + + const CBMType* obj_type = c_eval_expr_type(ctx, arg_node); + if (cbm_type_is_unknown(obj_type)) return cbm_type_unknown(); + + // Determine if this is . or -> access + // Check for "->" operator in node text + bool is_arrow = false; + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(node, i); + if (!ts_node_is_named(child)) { + char* op = c_node_text(ctx, child); + if (op && strcmp(op, "->") == 0) { is_arrow = true; break; } + } + } + + // Simplify type for member lookup + const CBMType* base; + if (is_arrow) { + base = c_simplify_type(ctx, obj_type, true); // unwrap pointer/smart ptr + } else { + base = c_simplify_type(ctx, obj_type, false); // just unwrap refs/aliases + } + + const char* type_qn = type_to_qn(base); + if (!type_qn) return cbm_type_unknown(); + + // Look up method + const CBMRegisteredFunc* method = c_lookup_member(ctx, type_qn, field_name); + if (method && method->signature) return method->signature; + + // Look up field + const CBMType* field_type = c_lookup_field_type(ctx, type_qn, field_name, 0); + if (field_type && !cbm_type_is_unknown(field_type)) return field_type; + + return cbm_type_unknown(); + } + + // --- qualified_identifier: ns::Class::method or Enum::Value --- + if (strcmp(kind, "qualified_identifier") == 0) { + char* text = c_node_text(ctx, node); + if (!text) return cbm_type_unknown(); + const char* qn = c_build_qn(ctx, text); + + // Try as function + const CBMRegisteredFunc* f = cbm_registry_lookup_func(ctx->registry, qn); + if (f && f->signature) return f->signature; + + // Try as type + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, qn); + if (rt) return cbm_type_named(ctx->arena, qn); + + // Try as enum member: Enum::Value → look up Enum type, return int/enum type + const char* dot = strrchr(qn, '.'); + if (dot) { + size_t prefix_len = (size_t)(dot - qn); + char* enum_qn = (char*)cbm_arena_alloc(ctx->arena, prefix_len + 1); + if (enum_qn) { + memcpy(enum_qn, qn, prefix_len); + enum_qn[prefix_len] = '\0'; + const CBMRegisteredType* enum_rt = cbm_registry_lookup_type(ctx->registry, enum_qn); + if (enum_rt) { + return cbm_type_named(ctx->arena, enum_qn); + } + // Try with module prefix + if (ctx->module_qn) { + const char* fqn_enum = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, enum_qn); + enum_rt = cbm_registry_lookup_type(ctx->registry, fqn_enum); + if (enum_rt) return cbm_type_named(ctx->arena, fqn_enum); + } + } + + // Template heuristic: T::member where T is a template param with default + if (ctx->in_template && dot) { + size_t prefix_len2 = (size_t)(dot - qn); + char* scope_name = (char*)cbm_arena_alloc(ctx->arena, prefix_len2 + 1); + if (scope_name) { + memcpy(scope_name, qn, prefix_len2); + scope_name[prefix_len2] = '\0'; + const CBMType* default_type = c_resolve_template_param(ctx, scope_name); + if (default_type) { + default_type = c_simplify_type(ctx, default_type, false); + const char* default_qn = type_to_qn(default_type); + if (default_qn) { + // Try as method: DefaultType::member + const char* member = dot + 1; + const CBMRegisteredFunc* mf = cbm_registry_lookup_method( + ctx->registry, default_qn, member); + if (mf && mf->signature) return mf->signature; + // Try as static function + const char* dep_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", + default_qn, member); + const CBMRegisteredFunc* sf = cbm_registry_lookup_func( + ctx->registry, dep_qn); + if (sf && sf->signature) return sf->signature; + } + } + } + } + } + + return cbm_type_unknown(); + } + + // --- template_function: make_shared, make_unique --- + // Returns a FUNC type whose return type is TEMPLATE(base_return, [explicit_args]). + if (strcmp(kind, "template_function") == 0) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + TSNode args_node = ts_node_child_by_field_name(node, "arguments", 9); + if (ts_node_is_null(name_node)) return cbm_type_unknown(); + + char* name = c_node_text(ctx, name_node); + if (!name) return cbm_type_unknown(); + + // Look up the function — handle both simple and qualified names + const CBMRegisteredFunc* f = NULL; + const char* nk = ts_node_type(name_node); + if (strcmp(nk, "qualified_identifier") == 0 || strcmp(nk, "scoped_identifier") == 0) { + // Qualified: std::make_shared → std.make_shared + const char* qn = c_build_qn(ctx, name); + f = cbm_registry_lookup_func(ctx->registry, qn); + if (!f && ctx->module_qn) { + const char* mod_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, qn); + f = cbm_registry_lookup_func(ctx->registry, mod_qn); + } + } else { + const char* fqn = c_resolve_name(ctx, name); + if (fqn) f = cbm_registry_lookup_func(ctx->registry, fqn); + } + if (!f || !f->signature) return cbm_type_unknown(); + + // Get the base return type from the function signature + const CBMType* base_ret = NULL; + if (f->signature->kind == CBM_TYPE_FUNC && + f->signature->data.func.return_types && + f->signature->data.func.return_types[0]) { + base_ret = f->signature->data.func.return_types[0]; + } + if (!base_ret) return f->signature; + + // Parse explicit template arguments from <...> + const CBMType* targs[16]; + int targ_count = 0; + if (!ts_node_is_null(args_node)) { + uint32_t nc = ts_node_named_child_count(args_node); + for (uint32_t i = 0; i < nc && targ_count < 15; i++) { + TSNode arg = ts_node_named_child(args_node, i); + if (ts_node_is_null(arg)) continue; + const char* ak = ts_node_type(arg); + if (strcmp(ak, "type_descriptor") == 0) { + if (ts_node_named_child_count(arg) > 0) + targs[targ_count++] = c_parse_type_node(ctx, ts_node_named_child(arg, 0)); + } else { + targs[targ_count++] = c_parse_type_node(ctx, arg); + } + } + } + + // Substitute explicit template args into return type + if (targ_count > 0) { + const char** tpn = f->type_param_names; + if (tpn) { + // Use registered type param names (e.g., ["T", NULL]) + base_ret = cbm_type_substitute(ctx->arena, base_ret, tpn, targs); + } else { + // Fallback: positional T, U, V + const char* fallback_names[] = {"T", "U", "V", "W", NULL}; + base_ret = cbm_type_substitute(ctx->arena, base_ret, fallback_names, targs); + } + } + + // Wrap return type in FUNC signature + const CBMType** rets = cbm_arena_alloc(ctx->arena, 2 * sizeof(CBMType*)); + rets[0] = base_ret; + rets[1] = NULL; + return cbm_type_func(ctx->arena, NULL, NULL, rets); + } + + // --- call_expression: f(args) --- + if (strcmp(kind, "call_expression") == 0) { + TSNode func_node = ts_node_child_by_field_name(node, "function", 8); + if (ts_node_is_null(func_node)) return cbm_type_unknown(); + + const CBMType* func_type = c_eval_expr_type(ctx, func_node); + if (!func_type) return cbm_type_unknown(); + + // FUNC type -> return its return type + if (func_type->kind == CBM_TYPE_FUNC && + func_type->data.func.return_types && func_type->data.func.return_types[0]) { + const CBMType* ret = func_type->data.func.return_types[0]; + + // Template param substitution: try receiver's template args (method calls) + // then try TAD from call-site argument types (free function calls) + bool needs_subst = (ret->kind == CBM_TYPE_TYPE_PARAM || + ret->kind == CBM_TYPE_REFERENCE || + ret->kind == CBM_TYPE_RVALUE_REF || + ret->kind == CBM_TYPE_TEMPLATE); + + if (needs_subst) { + bool substituted = false; + + // Strategy 1: method call on templated receiver — use receiver's template args + if (strcmp(ts_node_type(func_node), "field_expression") == 0) { + TSNode arg_node = ts_node_child_by_field_name(func_node, "argument", 8); + if (!ts_node_is_null(arg_node)) { + const CBMType* obj_type = c_eval_expr_type(ctx, arg_node); + obj_type = c_simplify_type(ctx, obj_type, true); + if (obj_type && obj_type->kind == CBM_TYPE_TEMPLATE && + obj_type->data.template_type.template_args) { + // Use registered type_param_names if available + TSNode field_node = ts_node_child_by_field_name(func_node, "field", 5); + char* method_name = !ts_node_is_null(field_node) ? + c_node_text(ctx, field_node) : NULL; + const char* recv_qn = type_to_qn(obj_type); + const CBMRegisteredFunc* mf = (recv_qn && method_name) ? + cbm_registry_lookup_method(ctx->registry, recv_qn, method_name) : NULL; + const char** tpn = mf ? mf->type_param_names : NULL; + + // Build substitution from receiver template args + int nargs = obj_type->data.template_type.arg_count; + if (tpn) { + // Use actual type param names from function registration + ret = cbm_type_substitute(ctx->arena, ret, tpn, + obj_type->data.template_type.template_args); + substituted = true; + } else { + // Fallback: positional params T, K, V, T1, T2 + const char* params[] = {"T", "K", "V", "T1", "T2", NULL}; + const CBMType* pargs[5] = {NULL}; + if (nargs > 0) pargs[0] = obj_type->data.template_type.template_args[0]; + if (nargs > 0) pargs[1] = obj_type->data.template_type.template_args[0]; + if (nargs > 1) pargs[2] = obj_type->data.template_type.template_args[1]; + if (nargs > 0) pargs[3] = obj_type->data.template_type.template_args[0]; + if (nargs > 1) pargs[4] = obj_type->data.template_type.template_args[1]; + ret = cbm_type_substitute(ctx->arena, ret, params, pargs); + substituted = true; + } + } + } + } + + // Strategy 2: TAD — deduce template params from call-site argument types + if (!substituted) { + // Find the registered function to get its type_param_names and param_types + const CBMRegisteredFunc* rf = NULL; + const char* fn_type = ts_node_type(func_node); + if (strcmp(fn_type, "identifier") == 0) { + char* fname = c_node_text(ctx, func_node); + if (fname) { + const char* fqn = c_resolve_name(ctx, fname); + if (fqn) rf = cbm_registry_lookup_func(ctx->registry, fqn); + } + } else if (strcmp(fn_type, "qualified_identifier") == 0 || + strcmp(fn_type, "scoped_identifier") == 0) { + char* fname = c_node_text(ctx, func_node); + if (fname) { + const char* qn = c_build_qn(ctx, fname); + rf = cbm_registry_lookup_func(ctx->registry, qn); + } + } + + if (rf && rf->type_param_names && rf->signature && + rf->signature->kind == CBM_TYPE_FUNC && + rf->signature->data.func.param_types) { + // Deduce: match call-site arg types against param types + // to bind type_param_names + int tp_count = 0; + while (rf->type_param_names[tp_count]) tp_count++; + if (tp_count > 0 && tp_count <= 8) { + const CBMType* deduced[8] = {NULL}; + TSNode call_args = ts_node_child_by_field_name(node, "arguments", 9); + int pi = 0; + if (!ts_node_is_null(call_args)) { + uint32_t anc = ts_node_named_child_count(call_args); + for (uint32_t ai = 0; ai < anc && rf->signature->data.func.param_types[pi]; ai++) { + TSNode carg = ts_node_named_child(call_args, ai); + if (ts_node_is_null(carg)) continue; + const CBMType* arg_t = c_eval_expr_type(ctx, carg); + if (cbm_type_is_unknown(arg_t)) { pi++; continue; } + // Unwrap ref/ptr from param type + const CBMType* param_t = rf->signature->data.func.param_types[pi]; + while (param_t && (param_t->kind == CBM_TYPE_REFERENCE || + param_t->kind == CBM_TYPE_RVALUE_REF || + param_t->kind == CBM_TYPE_POINTER)) { + if (param_t->kind == CBM_TYPE_POINTER) param_t = param_t->data.pointer.elem; + else param_t = param_t->data.reference.elem; + } + // If param type is TYPE_PARAM, bind it + if (param_t && param_t->kind == CBM_TYPE_TYPE_PARAM) { + for (int ti = 0; ti < tp_count; ti++) { + if (strcmp(rf->type_param_names[ti], param_t->data.type_param.name) == 0) { + // Unwrap ref/ptr from actual arg too + const CBMType* unwrapped = arg_t; + while (unwrapped && (unwrapped->kind == CBM_TYPE_REFERENCE || + unwrapped->kind == CBM_TYPE_RVALUE_REF || + unwrapped->kind == CBM_TYPE_POINTER)) { + if (unwrapped->kind == CBM_TYPE_POINTER) unwrapped = unwrapped->data.pointer.elem; + else unwrapped = unwrapped->data.reference.elem; + } + if (!deduced[ti]) deduced[ti] = unwrapped; + break; + } + } + } + pi++; + } + } + // Apply deduced substitutions + bool any_deduced = false; + for (int ti = 0; ti < tp_count; ti++) { + if (deduced[ti]) { any_deduced = true; break; } + } + if (any_deduced) { + ret = cbm_type_substitute(ctx->arena, ret, + rf->type_param_names, deduced); + } + } + } + } + } + + // Unwrap references in return type + if (ret->kind == CBM_TYPE_REFERENCE || ret->kind == CBM_TYPE_RVALUE_REF) + ret = ret->data.reference.elem; + return ret; + } + + // Constructor call: Type(args) — if func_node resolves to a named type + if (func_type->kind == CBM_TYPE_NAMED) { + return func_type; + } + + return cbm_type_unknown(); + } + + // --- new_expression: new Foo(args) --- + if (strcmp(kind, "new_expression") == 0) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(type_node)) { + return cbm_type_pointer(ctx->arena, c_parse_type_node(ctx, type_node)); + } + // Try first named child + 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, "type_identifier") == 0 || strcmp(ck, "template_type") == 0 || + strcmp(ck, "scoped_type_identifier") == 0 || strcmp(ck, "primitive_type") == 0) { + return cbm_type_pointer(ctx->arena, c_parse_type_node(ctx, child)); + } + } + return cbm_type_pointer(ctx->arena, cbm_type_unknown()); + } + + // --- delete_expression --- + if (strcmp(kind, "delete_expression") == 0) { + return cbm_type_unknown(); // void + } + + // --- subscript_expression: a[i] --- + if (strcmp(kind, "subscript_expression") == 0) { + TSNode arg_node = ts_node_child_by_field_name(node, "argument", 8); + if (ts_node_is_null(arg_node)) return cbm_type_unknown(); + const CBMType* arr_type = c_eval_expr_type(ctx, arg_node); + if (!arr_type) return cbm_type_unknown(); + + // Array/slice: return element + if (arr_type->kind == CBM_TYPE_SLICE) return arr_type->data.slice.elem; + if (arr_type->kind == CBM_TYPE_POINTER) return arr_type->data.pointer.elem; + + // Template with operator[]: return via method lookup + const char* qn = type_to_qn(c_simplify_type(ctx, arr_type, false)); + if (qn) { + const CBMRegisteredFunc* op = cbm_registry_lookup_method(ctx->registry, qn, "operator[]"); + if (op && op->signature && op->signature->kind == CBM_TYPE_FUNC && + op->signature->data.func.return_types && op->signature->data.func.return_types[0]) { + const CBMType* ret = op->signature->data.func.return_types[0]; + if (ret->kind == CBM_TYPE_REFERENCE) ret = ret->data.reference.elem; + + // Substitute template params + if (ret->kind == CBM_TYPE_TYPE_PARAM && arr_type->kind == CBM_TYPE_TEMPLATE) { + const char* params[] = {"T", "K", "V", NULL}; + const CBMType* args[3] = {NULL}; + int nargs = arr_type->data.template_type.arg_count; + if (nargs > 0) args[0] = arr_type->data.template_type.template_args[0]; + if (nargs > 0) args[1] = arr_type->data.template_type.template_args[0]; + if (nargs > 1) args[2] = arr_type->data.template_type.template_args[1]; + ret = cbm_type_substitute(ctx->arena, ret, params, args); + } + return ret; + } + } + return cbm_type_unknown(); + } + + // --- cast expressions --- + if (strcmp(kind, "cast_expression") == 0) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(type_node)) return c_parse_type_node(ctx, type_node); + // Fallback: first named child might be type_descriptor + if (ts_node_named_child_count(node) > 0) { + TSNode first = ts_node_named_child(node, 0); + if (strcmp(ts_node_type(first), "type_descriptor") == 0) + return c_parse_type_node(ctx, first); + } + return cbm_type_unknown(); + } + if (strcmp(kind, "static_cast_expression") == 0 || + strcmp(kind, "dynamic_cast_expression") == 0 || + strcmp(kind, "reinterpret_cast_expression") == 0 || + strcmp(kind, "const_cast_expression") == 0) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(type_node)) return c_parse_type_node(ctx, type_node); + return cbm_type_unknown(); + } + + // --- unary_expression: *p, &x, !x, -x --- + if (strcmp(kind, "unary_expression") == 0) { + TSNode operand = ts_node_child_by_field_name(node, "argument", 8); + if (ts_node_is_null(operand)) { + // Try "operand" field name + operand = ts_node_child_by_field_name(node, "operand", 7); + } + if (ts_node_is_null(operand)) return cbm_type_unknown(); + + // Get operator from first non-named child + for (uint32_t i = 0; i < ts_node_child_count(node); i++) { + TSNode child = ts_node_child(node, i); + if (!ts_node_is_named(child)) { + char* op = c_node_text(ctx, child); + if (!op) continue; + if (strcmp(op, "*") == 0) { + const CBMType* inner = c_eval_expr_type(ctx, operand); + return cbm_type_deref(inner); + } + if (strcmp(op, "&") == 0) { + return cbm_type_pointer(ctx->arena, c_eval_expr_type(ctx, operand)); + } + if (strcmp(op, "!") == 0 || strcmp(op, "~") == 0) { + return c_eval_expr_type(ctx, operand); + } + if (strcmp(op, "-") == 0 || strcmp(op, "+") == 0) { + return c_eval_expr_type(ctx, operand); + } + break; + } + } + return cbm_type_unknown(); + } + + // --- pointer_expression: *p or &x (C tree-sitter uses this instead of unary_expression) --- + if (strcmp(kind, "pointer_expression") == 0) { + TSNode arg = ts_node_child_by_field_name(node, "argument", 8); + if (ts_node_is_null(arg)) return cbm_type_unknown(); + // Check operator + for (uint32_t i = 0; i < ts_node_child_count(node); i++) { + TSNode child = ts_node_child(node, i); + if (!ts_node_is_named(child)) { + char* op = c_node_text(ctx, child); + if (op && strcmp(op, "*") == 0) return cbm_type_deref(c_eval_expr_type(ctx, arg)); + if (op && strcmp(op, "&") == 0) return cbm_type_pointer(ctx->arena, c_eval_expr_type(ctx, arg)); + break; + } + } + return cbm_type_unknown(); + } + + // --- update_expression: ++i, i++ --- + if (strcmp(kind, "update_expression") == 0) { + TSNode operand = ts_node_child_by_field_name(node, "argument", 8); + if (!ts_node_is_null(operand)) return c_eval_expr_type(ctx, operand); + return cbm_type_unknown(); + } + + // --- binary_expression --- + if (strcmp(kind, "binary_expression") == 0) { + TSNode left = ts_node_child_by_field_name(node, "left", 4); + // Check operator for comparison (returns bool) + for (uint32_t i = 0; i < ts_node_child_count(node); i++) { + TSNode child = ts_node_child(node, i); + if (!ts_node_is_named(child)) { + char* op = c_node_text(ctx, child); + if (op && (strcmp(op, "==") == 0 || strcmp(op, "!=") == 0 || + strcmp(op, "<") == 0 || strcmp(op, ">") == 0 || + strcmp(op, "<=") == 0 || strcmp(op, ">=") == 0 || + strcmp(op, "&&") == 0 || strcmp(op, "||") == 0)) { + return cbm_type_builtin(ctx->arena, "bool"); + } + break; + } + } + if (!ts_node_is_null(left)) return c_eval_expr_type(ctx, left); + return cbm_type_unknown(); + } + + // --- conditional_expression: a ? b : c --- + if (strcmp(kind, "conditional_expression") == 0) { + TSNode consequence = ts_node_child_by_field_name(node, "consequence", 11); + if (!ts_node_is_null(consequence)) return c_eval_expr_type(ctx, consequence); + return cbm_type_unknown(); + } + + // --- parenthesized_expression: (expr) --- + if (strcmp(kind, "parenthesized_expression") == 0 && ts_node_named_child_count(node) > 0) { + return c_eval_expr_type(ctx, ts_node_named_child(node, 0)); + } + + // --- comma_expression --- + if (strcmp(kind, "comma_expression") == 0) { + TSNode right = ts_node_child_by_field_name(node, "right", 5); + if (!ts_node_is_null(right)) return c_eval_expr_type(ctx, right); + return cbm_type_unknown(); + } + + // --- sizeof / alignof --- + if (strcmp(kind, "sizeof_expression") == 0 || strcmp(kind, "alignof_expression") == 0) { + return cbm_type_builtin(ctx->arena, "size_t"); + } + + // --- lambda_expression --- + if (strcmp(kind, "lambda_expression") == 0) { + const CBMType* ret_type = NULL; + + // Phase A: trailing return type on declarator (e.g., [](int x) -> Widget { ... }) + TSNode declarator = ts_node_child_by_field_name(node, "declarator", 10); + if (!ts_node_is_null(declarator)) { + uint32_t dnc = ts_node_named_child_count(declarator); + for (uint32_t di = 0; di < dnc; di++) { + TSNode ch = ts_node_named_child(declarator, di); + if (strcmp(ts_node_type(ch), "trailing_return_type") == 0) { + TSNode type_desc = ts_node_named_child_count(ch) > 0 ? + ts_node_named_child(ch, 0) : ch; + ret_type = c_parse_type_node(ctx, type_desc); + break; + } + } + } + + // Phase B: infer from body's immediate return statements (no descent into nested lambdas) + if (!ret_type || cbm_type_is_unknown(ret_type)) { + TSNode body = ts_node_child_by_field_name(node, "body", 4); + if (!ts_node_is_null(body)) { + uint32_t bnc = ts_node_named_child_count(body); + for (uint32_t bi = 0; bi < bnc; bi++) { + TSNode stmt = ts_node_named_child(body, bi); + if (ts_node_is_null(stmt)) continue; + const char* sk = ts_node_type(stmt); + // Skip nested lambdas, loops, etc. — only immediate returns + if (strcmp(sk, "return_statement") == 0) { + if (ts_node_named_child_count(stmt) > 0) { + TSNode ret_expr = ts_node_named_child(stmt, 0); + ret_type = c_eval_expr_type(ctx, ret_expr); + } + break; // first return only + } + } + } + } + + if (ret_type && !cbm_type_is_unknown(ret_type)) { + const CBMType** ret_types = (const CBMType**)cbm_arena_alloc(ctx->arena, 2 * sizeof(const CBMType*)); + if (ret_types) { + ret_types[0] = ret_type; + ret_types[1] = NULL; + return cbm_type_func(ctx->arena, NULL, NULL, ret_types); + } + } + return cbm_type_func(ctx->arena, NULL, NULL, NULL); + } + + // --- Literals --- + if (strcmp(kind, "number_literal") == 0) { + char* text = c_node_text(ctx, node); + if (text) { + // Check for float suffix or decimal point + for (const char* p = text; *p; p++) { + if (*p == '.' || *p == 'f' || *p == 'F' || *p == 'e' || *p == 'E') + return cbm_type_builtin(ctx->arena, "double"); + } + } + return cbm_type_builtin(ctx->arena, "int"); + } + if (strcmp(kind, "string_literal") == 0 || strcmp(kind, "concatenated_string") == 0 || + strcmp(kind, "raw_string_literal") == 0) { + // C and C++ string literals are const char*, not std::string + // (std::string requires explicit construction or "hello"s suffix) + return cbm_type_pointer(ctx->arena, cbm_type_builtin(ctx->arena, "char")); + } + if (strcmp(kind, "char_literal") == 0) { + return cbm_type_builtin(ctx->arena, "char"); + } + if (strcmp(kind, "true") == 0 || strcmp(kind, "false") == 0) { + return cbm_type_builtin(ctx->arena, "bool"); + } + if (strcmp(kind, "null") == 0 || strcmp(kind, "nullptr") == 0) { + return cbm_type_pointer(ctx->arena, cbm_type_unknown()); + } + + // --- compound_literal_expression: (Type){...} --- + if (strcmp(kind, "compound_literal_expression") == 0) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(type_node)) return c_parse_type_node(ctx, type_node); + return cbm_type_unknown(); + } + + // --- assignment_expression: a = b (returns LHS type) --- + if (strcmp(kind, "assignment_expression") == 0) { + TSNode left = ts_node_child_by_field_name(node, "left", 4); + if (!ts_node_is_null(left)) return c_eval_expr_type(ctx, left); + return cbm_type_unknown(); + } + + // --- initializer_list: {1, 2, 3} --- + if (strcmp(kind, "initializer_list") == 0) { + return cbm_type_unknown(); // context-dependent + } + + return cbm_type_unknown(); +} + +// ============================================================================ +// c_lookup_member: method/field lookup with base class traversal +// ============================================================================ + +const CBMRegisteredFunc* c_lookup_member(CLSPContext* ctx, const char* type_qn, const char* member_name) { + if (!type_qn || !member_name) return NULL; + + // Direct method lookup + const CBMRegisteredFunc* f = cbm_registry_lookup_method(ctx->registry, type_qn, member_name); + if (f) return f; + + // Try module-prefixed QN (e.g., "std.vector" -> "test.std.vector") + if (ctx->module_qn && !strchr(type_qn, '.')) { + // Short name without dots — unlikely to need prefix + } else if (ctx->module_qn) { + const char* prefixed = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, type_qn); + f = cbm_registry_lookup_method(ctx->registry, prefixed, member_name); + if (f) return f; + } + + // Check registered type for alias and base classes + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn); + if (!rt && ctx->module_qn) { + rt = cbm_registry_lookup_type(ctx->registry, + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, type_qn)); + } + if (rt) { + // Alias chain + if (rt->alias_of) { + f = c_lookup_member(ctx, rt->alias_of, member_name); + if (f) return f; + } + + // Base classes (embedded_types stores base class QNs) + if (rt->embedded_types) { + for (int i = 0; rt->embedded_types[i]; i++) { + f = c_lookup_member(ctx, rt->embedded_types[i], member_name); + if (f) return f; + } + } + } + + return NULL; +} + +// Field type lookup +static const CBMType* c_lookup_field_type(CLSPContext* ctx, const char* type_qn, + const char* field_name, int depth) { + if (!type_qn || !field_name || depth > 5) return NULL; + + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn); + if (!rt && ctx->module_qn) { + rt = cbm_registry_lookup_type(ctx->registry, + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, type_qn)); + } + if (!rt) return NULL; + + if (rt->alias_of) return c_lookup_field_type(ctx, rt->alias_of, field_name, depth + 1); + + if (rt->field_names) { + for (int i = 0; rt->field_names[i]; i++) { + if (strcmp(rt->field_names[i], field_name) == 0 && rt->field_types && rt->field_types[i]) + return rt->field_types[i]; + } + } + + // Base classes + if (rt->embedded_types) { + for (int i = 0; rt->embedded_types[i]; i++) { + const CBMType* f = c_lookup_field_type(ctx, rt->embedded_types[i], field_name, depth + 1); + if (f) return f; + } + } + return NULL; +} + +// ============================================================================ +// c_process_statement: bind variables from statements +// ============================================================================ + +// Parse a declaration to extract type and declarators +static const CBMType* c_parse_declaration_type(CLSPContext* ctx, TSNode decl_node) { + // Look for type node in declaration children + uint32_t nc = ts_node_named_child_count(decl_node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_named_child(decl_node, i); + const char* ck = ts_node_type(child); + + // Skip declarators + if (strcmp(ck, "init_declarator") == 0 || strcmp(ck, "identifier") == 0 || + strcmp(ck, "pointer_declarator") == 0 || strcmp(ck, "reference_declarator") == 0 || + strcmp(ck, "array_declarator") == 0 || strcmp(ck, "function_declarator") == 0) + continue; + + // Found a type node + return c_parse_type_node(ctx, child); + } + return cbm_type_unknown(); +} + +void c_process_statement(CLSPContext* ctx, TSNode node) { + if (ts_node_is_null(node)) return; + const char* kind = ts_node_type(node); + + // declaration: Type var = expr; or Type var1, var2; + if (strcmp(kind, "declaration") == 0) { + const CBMType* base_type = c_parse_declaration_type(ctx, node); + bool has_auto = false; + + // Check if type is auto/decltype + 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, "placeholder_type_specifier") == 0 || strcmp(ck, "auto") == 0) { + has_auto = true; + break; + } + if (strcmp(ck, "decltype") == 0) { + base_type = c_parse_type_node(ctx, child); + break; + } + } + + // Process each declarator + 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, "init_declarator") == 0) { + TSNode decl = ts_node_child_by_field_name(child, "declarator", 10); + TSNode value = ts_node_child_by_field_name(child, "value", 5); + + const CBMType* var_type = base_type; + + // For auto, infer from initializer + if (has_auto && !ts_node_is_null(value)) { + var_type = c_eval_expr_type(ctx, value); + } + + // Get variable name from declarator + if (!ts_node_is_null(decl)) { + const char* dk = ts_node_type(decl); + char* var_name = NULL; + + if (strcmp(dk, "identifier") == 0) { + var_name = c_node_text(ctx, decl); + } else if (strcmp(dk, "pointer_declarator") == 0) { + // Count pointer depth and find identifier + int ptr_depth = 0; + TSNode inner = decl; + while (!ts_node_is_null(inner) && + strcmp(ts_node_type(inner), "pointer_declarator") == 0) { + ptr_depth++; + uint32_t dnc = ts_node_named_child_count(inner); + inner = dnc > 0 ? ts_node_named_child(inner, dnc - 1) : (TSNode){0}; + } + if (!ts_node_is_null(inner) && strcmp(ts_node_type(inner), "identifier") == 0) + var_name = c_node_text(ctx, inner); + for (int d = 0; d < ptr_depth; d++) + var_type = cbm_type_pointer(ctx->arena, var_type); + } else if (strcmp(dk, "reference_declarator") == 0) { + if (ts_node_named_child_count(decl) > 0) { + TSNode inner = ts_node_named_child(decl, 0); + if (strcmp(ts_node_type(inner), "identifier") == 0) + var_name = c_node_text(ctx, inner); + } + var_type = cbm_type_reference(ctx->arena, var_type); + } else if (strcmp(dk, "function_declarator") == 0) { + // Function pointer: int (*fp)(int) = &target_func; + // Walk into function_declarator → parenthesized_declarator → pointer_declarator → identifier + TSNode inner = ts_node_child_by_field_name(decl, "declarator", 10); + while (!ts_node_is_null(inner)) { + const char* ik = ts_node_type(inner); + if (strcmp(ik, "identifier") == 0) { + var_name = c_node_text(ctx, inner); + break; + } else if (strcmp(ik, "parenthesized_declarator") == 0 || + strcmp(ik, "pointer_declarator") == 0) { + // Walk deeper: get last named child + uint32_t nc = ts_node_named_child_count(inner); + inner = nc > 0 ? ts_node_named_child(inner, nc - 1) : (TSNode){0}; + } else { + break; + } + } + // Mark as pointer type (function pointer decays to pointer) + var_type = cbm_type_pointer(ctx->arena, var_type); + } else if (strcmp(dk, "array_declarator") == 0) { + // Type var[N]; — extract name and wrap in slice + TSNode inner = ts_node_child_by_field_name(decl, "declarator", 10); + if (!ts_node_is_null(inner) && strcmp(ts_node_type(inner), "identifier") == 0) { + var_name = c_node_text(ctx, inner); + } + var_type = cbm_type_slice(ctx->arena, var_type); + } else if (strcmp(dk, "structured_binding_declarator") == 0) { + // auto [a, b] = expr; + // Try to decompose pair/tuple from initializer type + const CBMType* rhs_type = cbm_type_unknown(); + if (!ts_node_is_null(value)) { + rhs_type = c_eval_expr_type(ctx, value); + rhs_type = c_simplify_type(ctx, rhs_type, false); + } + uint32_t bnc = ts_node_named_child_count(decl); + int binding_idx = 0; + for (uint32_t bi = 0; bi < bnc; bi++) { + TSNode bn = ts_node_named_child(decl, bi); + if (strcmp(ts_node_type(bn), "identifier") == 0) { + char* bname = c_node_text(ctx, bn); + if (!bname) continue; + const CBMType* elem_type = cbm_type_unknown(); + // Decompose std::pair → first=T1, second=T2 + if (rhs_type && rhs_type->kind == CBM_TYPE_TEMPLATE && + rhs_type->data.template_type.template_args) { + int nta = rhs_type->data.template_type.arg_count; + if (binding_idx < nta) { + elem_type = rhs_type->data.template_type.template_args[binding_idx]; + } + } + // Decompose struct fields + if (cbm_type_is_unknown(elem_type) && rhs_type) { + const char* rhs_qn = type_to_qn(rhs_type); + if (rhs_qn) { + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, rhs_qn); + if (rt && rt->field_types && rt->field_types[binding_idx]) { + elem_type = rt->field_types[binding_idx]; + } + } + } + cbm_scope_bind(ctx->current_scope, bname, elem_type); + binding_idx++; + } + } + continue; + } + + if (var_name && var_name[0] && strcmp(var_name, "_") != 0) { + cbm_scope_bind(ctx->current_scope, var_name, var_type); + + // Track function pointer targets: fp = &foo or fp = foo + // Accept any pointer or func type — C function pointer declarators + // like int (*fp)(int) often parse as pointer(int), not pointer(FUNC). + if (var_type && (var_type->kind == CBM_TYPE_FUNC || + var_type->kind == CBM_TYPE_POINTER)) { + if (!ts_node_is_null(value)) { + const char* vk = ts_node_type(value); + // pointer_expression with & operator: &foo + if (strcmp(vk, "pointer_expression") == 0 || + strcmp(vk, "unary_expression") == 0) { + uint32_t vnc = ts_node_named_child_count(value); + for (uint32_t vi = 0; vi < vnc; vi++) { + TSNode vch = ts_node_named_child(value, vi); + if (strcmp(ts_node_type(vch), "identifier") == 0) { + char* target_name = c_node_text(ctx, vch); + if (target_name) { + const char* target_qn = c_resolve_name_to_func_qn(ctx, target_name); + if (target_qn) { + c_add_fp_target(ctx, var_name, target_qn); + } + } + break; + } + } + } + // Direct identifier: fp = foo (function decays to pointer) + else if (strcmp(vk, "identifier") == 0) { + char* target_name = c_node_text(ctx, value); + if (target_name) { + const char* target_qn = c_resolve_name_to_func_qn(ctx, target_name); + if (target_qn) { + c_add_fp_target(ctx, var_name, target_qn); + } + } + } + } + } + } + } + } else if (strcmp(ck, "identifier") == 0) { + // Bare declaration without initializer: Type var; + char* var_name = c_node_text(ctx, child); + if (var_name && var_name[0]) { + cbm_scope_bind(ctx->current_scope, var_name, base_type); + } + } else if (strcmp(ck, "pointer_declarator") == 0) { + // Type *var; without initializer + int ptr_depth = 0; + TSNode inner = child; + while (!ts_node_is_null(inner) && + strcmp(ts_node_type(inner), "pointer_declarator") == 0) { + ptr_depth++; + uint32_t dnc = ts_node_named_child_count(inner); + inner = dnc > 0 ? ts_node_named_child(inner, dnc - 1) : (TSNode){0}; + } + if (!ts_node_is_null(inner) && strcmp(ts_node_type(inner), "identifier") == 0) { + char* var_name = c_node_text(ctx, inner); + if (var_name) { + const CBMType* vt = base_type; + for (int d = 0; d < ptr_depth; d++) + vt = cbm_type_pointer(ctx->arena, vt); + cbm_scope_bind(ctx->current_scope, var_name, vt); + } + } + } + } + return; + } + + // using_declaration: using namespace std; or using std::cout; + if (strcmp(kind, "using_declaration") == 0) { + char* text = c_node_text(ctx, node); + if (!text) return; + + // "using namespace XXX;" + if (strstr(text, "namespace")) { + // Extract namespace name after "namespace" + 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, "identifier") == 0 || strcmp(ck, "qualified_identifier") == 0 || + strcmp(ck, "scoped_identifier") == 0) { + char* ns_name = c_node_text(ctx, child); + if (ns_name) c_add_using_namespace(ctx, c_build_qn(ctx, ns_name)); + } + } + } else if (strstr(text, "enum")) { + // "using enum MyEnum;" — import enum members into scope + 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, "identifier") == 0 || strcmp(ck, "qualified_identifier") == 0 || + strcmp(ck, "scoped_identifier") == 0 || strcmp(ck, "type_identifier") == 0) { + char* enum_text = c_node_text(ctx, child); + if (enum_text) { + const char* enum_qn = c_build_qn(ctx, enum_text); + // Try to find the enum type in registry, import its members + const CBMRegisteredType* et = cbm_registry_lookup_type(ctx->registry, enum_qn); + if (!et && ctx->module_qn) { + enum_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, enum_qn); + et = cbm_registry_lookup_type(ctx->registry, enum_qn); + } + if (!et && ctx->current_namespace) { + enum_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->current_namespace, enum_text); + et = cbm_registry_lookup_type(ctx->registry, enum_qn); + } + // Even if enum type not found, add as using namespace for name lookup + c_add_using_namespace(ctx, enum_qn); + } + } + } + } else { + // "using std::cout;" — specific declaration + 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, "qualified_identifier") == 0 || strcmp(ck, "scoped_identifier") == 0) { + char* full_text = c_node_text(ctx, child); + if (full_text) { + const char* qn = c_build_qn(ctx, full_text); + // Extract short name (last component) + const char* short_name = strrchr(qn, '.'); + short_name = short_name ? short_name + 1 : qn; + c_add_using_decl(ctx, short_name, qn); + } + } + } + } + return; + } + + // alias_declaration: using Foo = Bar; + if (strcmp(kind, "alias_declaration") == 0) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(name_node) && !ts_node_is_null(type_node)) { + char* alias_name = c_node_text(ctx, name_node); + const CBMType* target = c_parse_type_node(ctx, type_node); + if (alias_name && target) { + // Build alias QN + const char* alias_qn = ctx->current_namespace ? + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->current_namespace, alias_name) : + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, alias_name); + cbm_scope_bind(ctx->current_scope, alias_name, + cbm_type_alias(ctx->arena, alias_qn, target)); + } + } + return; + } + + // type_definition: typedef Bar Foo; + if (strcmp(kind, "type_definition") == 0) { + // Get the type being aliased + const CBMType* target = cbm_type_unknown(); + uint32_t nc = ts_node_named_child_count(node); + bool found_type = false; + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_named_child(node, i); + const char* ck = ts_node_type(child); + if (!found_type && strcmp(ck, "type_identifier") != 0 && + strcmp(ck, "pointer_declarator") != 0 && + strcmp(ck, "identifier") != 0) { + target = c_parse_type_node(ctx, child); + found_type = true; + } else if (found_type && strcmp(ck, "type_identifier") == 0) { + char* alias_name = c_node_text(ctx, child); + if (alias_name) { + const char* alias_qn = ctx->current_namespace ? + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->current_namespace, alias_name) : + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, alias_name); + cbm_scope_bind(ctx->current_scope, alias_name, + cbm_type_alias(ctx->arena, alias_qn, target)); + } + } + } + return; + } + + // namespace_alias_definition: namespace fs = std::filesystem; + if (strcmp(kind, "namespace_alias_definition") == 0) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + TSNode value_node = ts_node_child_by_field_name(node, "value", 5); + if (ts_node_is_null(value_node)) { + // Try last named child + uint32_t nc = ts_node_named_child_count(node); + if (nc >= 2) { + name_node = ts_node_named_child(node, 0); + value_node = ts_node_named_child(node, nc - 1); + } + } + if (!ts_node_is_null(name_node) && !ts_node_is_null(value_node)) { + char* alias = c_node_text(ctx, name_node); + char* target = c_node_text(ctx, value_node); + if (alias && target) { + c_add_ns_alias(ctx, alias, c_build_qn(ctx, target)); + } + } + return; + } + + // for_range_loop: for (auto& x : container) { ... } + if (strcmp(kind, "for_range_loop") == 0) { + TSNode decl = ts_node_child_by_field_name(node, "declarator", 10); + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + TSNode right = ts_node_child_by_field_name(node, "right", 5); + + const CBMType* elem_type = cbm_type_unknown(); + if (!ts_node_is_null(right)) { + const CBMType* container_type = c_eval_expr_type(ctx, right); + // Deduce element type: container's begin()->operator* or element type + if (container_type) { + if (container_type->kind == CBM_TYPE_SLICE) { + elem_type = container_type->data.slice.elem; + } else if (container_type->kind == CBM_TYPE_TEMPLATE && + container_type->data.template_type.arg_count > 0) { + // Map containers: element is pair not just K + const char* tname = container_type->data.template_type.template_name; + if (tname && container_type->data.template_type.arg_count >= 2 && + (strstr(tname, "map") || strstr(tname, "Map"))) { + const CBMType* pair_args[3]; + pair_args[0] = container_type->data.template_type.template_args[0]; + pair_args[1] = container_type->data.template_type.template_args[1]; + pair_args[2] = NULL; + elem_type = cbm_type_template(ctx->arena, "std.pair", + pair_args, 2); + } else { + elem_type = container_type->data.template_type.template_args[0]; + } + } + // Iterator protocol fallback: begin() -> iter -> operator*() -> elem + if (cbm_type_is_unknown(elem_type) && container_type->kind == CBM_TYPE_NAMED) { + const char* cqn = container_type->data.named.qualified_name; + const CBMRegisteredFunc* begin_fn = c_lookup_member(ctx, cqn, "begin"); + if (begin_fn && begin_fn->signature && + begin_fn->signature->kind == CBM_TYPE_FUNC && + begin_fn->signature->data.func.return_types && + begin_fn->signature->data.func.return_types[0]) { + const CBMType* iter_type = begin_fn->signature->data.func.return_types[0]; + const char* iter_qn = type_to_qn(iter_type); + if (iter_qn) { + const CBMRegisteredFunc* deref = c_lookup_member(ctx, iter_qn, "operator*"); + if (deref && deref->signature && + deref->signature->kind == CBM_TYPE_FUNC && + deref->signature->data.func.return_types && + deref->signature->data.func.return_types[0]) { + elem_type = deref->signature->data.func.return_types[0]; + } + } + } + } + } + } + + // If explicit type, use that + if (!ts_node_is_null(type_node)) { + const char* tk = ts_node_type(type_node); + if (strcmp(tk, "placeholder_type_specifier") != 0 && strcmp(tk, "auto") != 0) { + elem_type = c_parse_type_node(ctx, type_node); + } + } + + // Bind declarator + if (!ts_node_is_null(decl)) { + const char* dk = ts_node_type(decl); + if (strcmp(dk, "identifier") == 0) { + char* var_name = c_node_text(ctx, decl); + if (var_name) cbm_scope_bind(ctx->current_scope, var_name, elem_type); + } else if (strcmp(dk, "reference_declarator") == 0) { + if (ts_node_named_child_count(decl) > 0) { + TSNode inner = ts_node_named_child(decl, 0); + if (strcmp(ts_node_type(inner), "identifier") == 0) { + char* var_name = c_node_text(ctx, inner); + if (var_name) cbm_scope_bind(ctx->current_scope, var_name, elem_type); + } + } + } + } + return; + } + + // parameter_declaration: bind param + if (strcmp(kind, "parameter_declaration") == 0) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + TSNode decl = ts_node_child_by_field_name(node, "declarator", 10); + if (ts_node_is_null(type_node)) return; + + const CBMType* param_type = c_parse_type_node(ctx, type_node); + + if (!ts_node_is_null(decl)) { + const char* dk = ts_node_type(decl); + if (strcmp(dk, "identifier") == 0) { + char* name = c_node_text(ctx, decl); + if (name) cbm_scope_bind(ctx->current_scope, name, param_type); + } else if (strcmp(dk, "pointer_declarator") == 0) { + int ptr_depth = 0; + TSNode inner = decl; + while (!ts_node_is_null(inner) && strcmp(ts_node_type(inner), "pointer_declarator") == 0) { + ptr_depth++; + uint32_t dnc = ts_node_named_child_count(inner); + inner = dnc > 0 ? ts_node_named_child(inner, dnc - 1) : (TSNode){0}; + } + if (!ts_node_is_null(inner) && strcmp(ts_node_type(inner), "identifier") == 0) { + char* name = c_node_text(ctx, inner); + if (name) { + const CBMType* vt = param_type; + for (int d = 0; d < ptr_depth; d++) vt = cbm_type_pointer(ctx->arena, vt); + cbm_scope_bind(ctx->current_scope, name, vt); + } + } + } else if (strcmp(dk, "reference_declarator") == 0) { + if (ts_node_named_child_count(decl) > 0) { + TSNode inner = ts_node_named_child(decl, 0); + if (strcmp(ts_node_type(inner), "identifier") == 0) { + char* name = c_node_text(ctx, inner); + if (name) cbm_scope_bind(ctx->current_scope, name, + cbm_type_reference(ctx->arena, param_type)); + } + } + } + } + return; + } +} + +// ============================================================================ +// Emit helpers +// ============================================================================ + +static void c_emit_resolved_call(CLSPContext* ctx, const char* callee_qn, const char* strategy, float confidence) { + if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) return; + CBMResolvedCall rc; + rc.caller_qn = ctx->enclosing_func_qn; + rc.callee_qn = callee_qn; + rc.strategy = strategy; + rc.confidence = confidence; + rc.reason = NULL; + cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); +} + +static void c_emit_unresolved_call(CLSPContext* ctx, const char* expr_text, const char* reason) { + if (!ctx->resolved_calls || !ctx->enclosing_func_qn) return; + CBMResolvedCall rc; + rc.caller_qn = ctx->enclosing_func_qn; + rc.callee_qn = expr_text ? expr_text : "?"; + rc.strategy = "lsp_unresolved"; + rc.confidence = 0.0f; + rc.reason = reason; + cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); +} + +// ============================================================================ +// resolve_calls_in_node: walk AST and resolve calls +// ============================================================================ + +static void c_resolve_calls_in_node(CLSPContext* ctx, TSNode node) { + if (ts_node_is_null(node)) return; + const char* kind = ts_node_type(node); + + // Process statements for scope building + c_process_statement(ctx, node); + + // --- Resolve call expressions --- + if (strcmp(kind, "call_expression") == 0) { + TSNode func_node = ts_node_child_by_field_name(node, "function", 8); + if (!ts_node_is_null(func_node)) { + const char* fk = ts_node_type(func_node); + + // field_expression: obj.method() or ptr->method() + if (strcmp(fk, "field_expression") == 0) { + TSNode arg_node = ts_node_child_by_field_name(func_node, "argument", 8); + TSNode field_node = ts_node_child_by_field_name(func_node, "field", 5); + if (!ts_node_is_null(arg_node) && !ts_node_is_null(field_node)) { + char* field_name = c_node_text(ctx, field_node); + if (field_name) { + const CBMType* obj_type = c_eval_expr_type(ctx, arg_node); + + // Determine if arrow or dot + bool is_arrow = false; + uint32_t fnc = ts_node_child_count(func_node); + for (uint32_t fi = 0; fi < fnc; fi++) { + TSNode ch = ts_node_child(func_node, fi); + if (!ts_node_is_named(ch)) { + char* op = c_node_text(ctx, ch); + if (op && strcmp(op, "->") == 0) { is_arrow = true; break; } + } + } + + const CBMType* base = is_arrow ? + c_simplify_type(ctx, obj_type, true) : + c_simplify_type(ctx, obj_type, false); + + const char* type_qn = type_to_qn(base); + if (type_qn) { + int arg_count = 0; + const CBMType** arg_types = c_extract_call_arg_types(ctx, node, &arg_count); + if (ctx->debug) fprintf(stderr, " [clsp] member call: type_qn=%s field=%s args=%d\n", type_qn, field_name, arg_count); + // Use type-aware overload scoring + const CBMRegisteredFunc* method = cbm_registry_lookup_method_by_types( + ctx->registry, type_qn, field_name, arg_types, arg_count); + // Fall back to c_lookup_member for base class traversal + if (!method) method = c_lookup_member(ctx, type_qn, field_name); + if (ctx->debug) fprintf(stderr, " [clsp] member call result: %s\n", method ? method->qualified_name : "NULL"); + if (method) { + const char* strategy = "lsp_type_dispatch"; + // Check if resolved through base class — prefer derived override + if (method->receiver_type && strcmp(method->receiver_type, type_qn) != 0) { + const CBMRegisteredFunc* override_m = cbm_registry_lookup_method( + ctx->registry, type_qn, field_name); + if (override_m) { + method = override_m; + strategy = "lsp_virtual_dispatch"; + } else { + strategy = "lsp_base_dispatch"; + } + } + // Check if through smart pointer + if (is_arrow && obj_type->kind == CBM_TYPE_TEMPLATE && + is_smart_ptr(obj_type->data.template_type.template_name)) + strategy = "lsp_smart_ptr_dispatch"; + c_emit_resolved_call(ctx, method->qualified_name, strategy, 0.95f); + goto recurse; + } + } + + // TYPE_PARAM receiver: store as pending template call + if (base && base->kind == CBM_TYPE_TYPE_PARAM && + ctx->enclosing_func_qn && ctx->in_template) { + int ac = 0; + c_extract_call_arg_types(ctx, node, &ac); + c_add_pending_template_call(ctx, + ctx->enclosing_func_qn, + base->data.type_param.name, + field_name, ac); + } + + // Unresolved + if (cbm_type_is_unknown(obj_type)) { + char* arg_text = c_node_text(ctx, arg_node); + c_emit_unresolved_call(ctx, + cbm_arena_sprintf(ctx->arena, "%s.%s", + arg_text ? arg_text : "?", field_name), + "unknown_receiver_type"); + } else if (type_qn) { + c_emit_unresolved_call(ctx, + cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, field_name), + "method_not_found"); + } + } + } + goto recurse; + } + + // qualified_identifier: ns::func() or Class::static_method() + if (strcmp(fk, "qualified_identifier") == 0 || + strcmp(fk, "scoped_identifier") == 0) { + char* text = c_node_text(ctx, func_node); + if (text) { + const char* qn = c_build_qn(ctx, text); + int arg_count = 0; + const CBMType** arg_types = c_extract_call_arg_types(ctx, node, &arg_count); + const CBMRegisteredFunc* f = cbm_registry_lookup_func(ctx->registry, qn); + if (!f && ctx->module_qn) { + const char* mod_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, qn); + f = cbm_registry_lookup_func(ctx->registry, mod_qn); + } + if (f) { + c_emit_resolved_call(ctx, f->qualified_name, "lsp_scoped", 0.95f); + goto recurse; + } + // Try as method (Class::method) with type-aware overload resolution + const char* dot = strrchr(qn, '.'); + if (dot) { + size_t prefix_len = dot - qn; + char* class_qn = (char*)cbm_arena_alloc(ctx->arena, prefix_len + 1); + memcpy(class_qn, qn, prefix_len); + class_qn[prefix_len] = '\0'; + const CBMRegisteredFunc* m = cbm_registry_lookup_method_by_types( + ctx->registry, class_qn, dot + 1, arg_types, arg_count); + // Try with module prefix + if (!m && ctx->module_qn) { + const char* mod_class = cbm_arena_sprintf(ctx->arena, "%s.%s", + ctx->module_qn, class_qn); + m = cbm_registry_lookup_method_by_types( + ctx->registry, mod_class, dot + 1, arg_types, arg_count); + } + if (m) { + c_emit_resolved_call(ctx, m->qualified_name, "lsp_scoped", 0.95f); + goto recurse; + } + } + c_emit_unresolved_call(ctx, qn, "scoped_not_in_registry"); + } + goto recurse; + } + + // template_function: func(args) or ns::func(args) + if (strcmp(fk, "template_function") == 0) { + TSNode name_node = ts_node_child_by_field_name(func_node, "name", 4); + if (!ts_node_is_null(name_node)) { + char* name = c_node_text(ctx, name_node); + if (name) { + const char* nk = ts_node_type(name_node); + const CBMRegisteredFunc* f = NULL; + if (strcmp(nk, "qualified_identifier") == 0 || + strcmp(nk, "scoped_identifier") == 0) { + const char* qn = c_build_qn(ctx, name); + f = cbm_registry_lookup_func(ctx->registry, qn); + if (!f && ctx->module_qn) { + const char* mod_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", + ctx->module_qn, qn); + f = cbm_registry_lookup_func(ctx->registry, mod_qn); + } + // Try as Class::method + if (!f) { + const char* dot = strrchr(qn, '.'); + if (dot) { + size_t plen = dot - qn; + char* cls = (char*)cbm_arena_alloc(ctx->arena, plen + 1); + memcpy(cls, qn, plen); + cls[plen] = '\0'; + const CBMRegisteredFunc* m = cbm_registry_lookup_method( + ctx->registry, cls, dot + 1); + if (!m && ctx->module_qn) { + const char* mc = cbm_arena_sprintf(ctx->arena, "%s.%s", + ctx->module_qn, cls); + m = cbm_registry_lookup_method(ctx->registry, mc, dot + 1); + } + if (m) f = m; + } + } + } else { + const char* fqn = c_resolve_name(ctx, name); + if (fqn) f = cbm_registry_lookup_func(ctx->registry, fqn); + } + if (f) { + c_emit_resolved_call(ctx, f->qualified_name, "lsp_template", 0.95f); + // Resolve pending template calls at this call site + if (ctx->pending_tc_count > 0 && f->type_param_names) { + int ac = 0; + const CBMType** at = c_extract_call_arg_types(ctx, node, &ac); + if (at) c_resolve_pending_template_calls(ctx, f, at, ac); + } + } else { + c_emit_unresolved_call(ctx, c_build_qn(ctx, name), "template_not_in_registry"); + } + } + } + goto recurse; + } + + // Direct identifier call: func() + if (strcmp(fk, "identifier") == 0) { + char* name = c_node_text(ctx, func_node); + if (name && !is_c_builtin_func(name)) { + // Check function pointer target map FIRST (before scope type check) + // because C function pointer declarators like int (*fp)(int) + // parse as pointer(int) in scope, not as FUNC type. + const char* fp_target = c_lookup_fp_target(ctx, name); + if (fp_target) { + c_emit_resolved_call(ctx, fp_target, "lsp_func_ptr", 0.85f); + goto recurse; + } + + // Check if it's a variable with callable type + const CBMType* var_type = cbm_scope_lookup(ctx->current_scope, name); + if (!cbm_type_is_unknown(var_type)) { + if (var_type->kind == CBM_TYPE_FUNC) { + // Function type variable without tracked target — can't resolve + goto recurse; + } + // Functor call: variable with operator() + const CBMType* base = c_simplify_type(ctx, var_type, false); + const char* type_qn = type_to_qn(base); + if (type_qn) { + const CBMRegisteredFunc* op = c_lookup_member(ctx, type_qn, "operator()"); + if (op) { + c_emit_resolved_call(ctx, op->qualified_name, "lsp_operator", 0.90f); + goto recurse; + } + } + } + + // Check if it's a type name (constructor call) + const CBMType* type = c_resolve_name_to_type(ctx, name); + if (type && type->kind == CBM_TYPE_NAMED) { + const char* type_qn_str = type->data.named.qualified_name; + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn_str); + if (rt && !rt->is_interface) { + // Constructor call + const char* ctor_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn_str, name); + c_emit_resolved_call(ctx, ctor_qn, "lsp_constructor", 0.95f); + goto recurse; + } + } + + // Regular function call + const char* fqn = c_resolve_name(ctx, name); + if (fqn) { + // Check if this is implicit 'this' call + const char* strategy = "lsp_direct"; + if (ctx->enclosing_class_qn) { + const CBMRegisteredFunc* m = cbm_registry_lookup_method(ctx->registry, + ctx->enclosing_class_qn, name); + if (m && strcmp(fqn, m->qualified_name) == 0) { + strategy = "lsp_implicit_this"; + } + } + c_emit_resolved_call(ctx, fqn, strategy, 0.95f); + // Resolve pending template calls at this call site + if (ctx->pending_tc_count > 0) { + const CBMRegisteredFunc* called = cbm_registry_lookup_func(ctx->registry, fqn); + if (called && called->type_param_names) { + int ac = 0; + const CBMType** at = c_extract_call_arg_types(ctx, node, &ac); + if (at) c_resolve_pending_template_calls(ctx, called, at, ac); + } + } + } else { + // ADL: search namespaces of argument types + const char* adl_qn = c_adl_resolve(ctx, name, node); + if (adl_qn) { + c_emit_resolved_call(ctx, adl_qn, "lsp_adl", 0.90f); + } else { + c_emit_unresolved_call(ctx, name, "function_not_in_registry"); + } + } + } + goto recurse; + } + } + } + + // --- Constructor calls from declarations --- + if (strcmp(kind, "declaration") == 0 && ctx->cpp_mode) { + // Check for Foo x(args) or Foo x{args} patterns + uint32_t nc = ts_node_named_child_count(node); + const CBMType* decl_type = cbm_type_unknown(); + bool has_type = false; + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_named_child(node, i); + const char* ck = ts_node_type(child); + if (!has_type && (strcmp(ck, "type_identifier") == 0 || + strcmp(ck, "scoped_type_identifier") == 0 || + strcmp(ck, "template_type") == 0)) { + decl_type = c_parse_type_node(ctx, child); + has_type = true; + } + if (has_type && strcmp(ck, "init_declarator") == 0) { + // Check if value is argument_list (constructor with parens) + TSNode value = ts_node_child_by_field_name(child, "value", 5); + if (!ts_node_is_null(value)) { + const char* vk = ts_node_type(value); + if (strcmp(vk, "argument_list") == 0 || strcmp(vk, "initializer_list") == 0) { + const char* type_qn = type_to_qn(decl_type); + if (type_qn) { + const char* short_name = strrchr(type_qn, '.'); + short_name = short_name ? short_name + 1 : type_qn; + const char* last_colon = strrchr(type_qn, ':'); + if (last_colon) short_name = last_colon + 1; + const char* ctor_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, short_name); + c_emit_resolved_call(ctx, ctor_qn, "lsp_constructor", 0.90f); + } + } + } + } + } + } + + // --- new_expression: emit constructor --- + if (strcmp(kind, "new_expression") == 0) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (ts_node_is_null(type_node)) { + 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, "type_identifier") == 0 || strcmp(ck, "template_type") == 0 || + strcmp(ck, "scoped_type_identifier") == 0) { + type_node = child; + break; + } + } + } + if (!ts_node_is_null(type_node)) { + const CBMType* t = c_parse_type_node(ctx, type_node); + const char* type_qn = type_to_qn(t); + if (type_qn) { + const char* short_name = strrchr(type_qn, '.'); + short_name = short_name ? short_name + 1 : type_qn; + const char* ctor_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, short_name); + c_emit_resolved_call(ctx, ctor_qn, "lsp_constructor", 0.95f); + } + } + } + + // --- delete_expression: emit destructor --- + if (strcmp(kind, "delete_expression") == 0 && ctx->cpp_mode) { + TSNode operand = ts_node_child_by_field_name(node, "argument", 8); + if (ts_node_is_null(operand)) { + if (ts_node_named_child_count(node) > 0) + operand = ts_node_named_child(node, 0); + } + if (!ts_node_is_null(operand)) { + const CBMType* ptr_type = c_eval_expr_type(ctx, operand); + const CBMType* base = c_simplify_type(ctx, ptr_type, true); + const char* type_qn = type_to_qn(base); + if (type_qn) { + const char* short_name = strrchr(type_qn, '.'); + short_name = short_name ? short_name + 1 : type_qn; + const char* dtor_qn = cbm_arena_sprintf(ctx->arena, "%s.~%s", type_qn, short_name); + c_emit_resolved_call(ctx, dtor_qn, "lsp_destructor", 0.90f); + } + } + } + + // --- Operator calls (C++ only) --- + if (ctx->cpp_mode && strcmp(kind, "binary_expression") == 0) { + TSNode left = ts_node_child_by_field_name(node, "left", 4); + if (!ts_node_is_null(left)) { + const CBMType* lhs_type = c_eval_expr_type(ctx, left); + const CBMType* base = c_simplify_type(ctx, lhs_type, false); + if (base && base->kind != CBM_TYPE_BUILTIN && !cbm_type_is_unknown(base)) { + const char* type_qn = type_to_qn(base); + if (type_qn) { + // Extract operator + for (uint32_t i = 0; i < ts_node_child_count(node); i++) { + TSNode child = ts_node_child(node, i); + if (!ts_node_is_named(child)) { + char* op = c_node_text(ctx, child); + if (op) { + const char* op_name = cbm_arena_sprintf(ctx->arena, "operator%s", op); + const CBMRegisteredFunc* m = c_lookup_member(ctx, type_qn, op_name); + if (m) { + c_emit_resolved_call(ctx, m->qualified_name, "lsp_operator", 0.90f); + } + } + break; + } + } + } + } + } + } + + // --- subscript_expression operator[] --- + if (ctx->cpp_mode && strcmp(kind, "subscript_expression") == 0) { + TSNode arg_node = ts_node_child_by_field_name(node, "argument", 8); + if (!ts_node_is_null(arg_node)) { + const CBMType* arr_type = c_eval_expr_type(ctx, arg_node); + const CBMType* base = c_simplify_type(ctx, arr_type, false); + if (base && base->kind != CBM_TYPE_BUILTIN && !cbm_type_is_unknown(base) && + base->kind != CBM_TYPE_POINTER && base->kind != CBM_TYPE_SLICE) { + const char* type_qn = type_to_qn(base); + if (type_qn) { + const CBMRegisteredFunc* m = c_lookup_member(ctx, type_qn, "operator[]"); + if (m) { + c_emit_resolved_call(ctx, m->qualified_name, "lsp_operator", 0.90f); + } + } + } + } + } + + // --- unary_expression/pointer_expression: operator* / operator++ / operator-- on custom types --- + if (ctx->cpp_mode && (strcmp(kind, "unary_expression") == 0 || + strcmp(kind, "pointer_expression") == 0 || + strcmp(kind, "update_expression") == 0)) { + TSNode operand = ts_node_child_by_field_name(node, "argument", 8); + if (ts_node_is_null(operand)) + operand = ts_node_child_by_field_name(node, "operand", 7); + if (!ts_node_is_null(operand)) { + const CBMType* op_type = c_eval_expr_type(ctx, operand); + const CBMType* base = c_simplify_type(ctx, op_type, false); + if (base && base->kind != CBM_TYPE_BUILTIN && !cbm_type_is_unknown(base) && + base->kind != CBM_TYPE_POINTER) { + const char* type_qn = type_to_qn(base); + if (type_qn) { + // Determine which operator + const char* op_name = NULL; + for (uint32_t ui = 0; ui < ts_node_child_count(node); ui++) { + TSNode ch = ts_node_child(node, ui); + if (!ts_node_is_named(ch)) { + char* op = c_node_text(ctx, ch); + if (!op) continue; + if (strcmp(op, "*") == 0) op_name = "operator*"; + else if (strcmp(op, "++") == 0) op_name = "operator++"; + else if (strcmp(op, "--") == 0) op_name = "operator--"; + else if (strcmp(op, "!") == 0) op_name = "operator!"; + break; + } + } + if (op_name) { + const CBMRegisteredFunc* m = c_lookup_member(ctx, type_qn, op_name); + if (m) { + c_emit_resolved_call(ctx, m->qualified_name, "lsp_operator", 0.90f); + } + } + } + } + } + } + + // --- Copy/move constructor: Foo a = expr; where expr has Foo type --- + if (ctx->cpp_mode && strcmp(kind, "declaration") == 0) { + const CBMType* decl_type = c_parse_declaration_type(ctx, node); + if (decl_type && decl_type->kind == CBM_TYPE_NAMED) { + uint32_t dnc = ts_node_named_child_count(node); + for (uint32_t di = 0; di < dnc; di++) { + TSNode dchild = ts_node_named_child(node, di); + if (strcmp(ts_node_type(dchild), "init_declarator") == 0) { + TSNode val = ts_node_child_by_field_name(dchild, "value", 5); + if (!ts_node_is_null(val)) { + const char* vk = ts_node_type(val); + // Skip if it's argument_list (already handled as constructor above) + // or initializer_list (also handled) + if (strcmp(vk, "argument_list") != 0 && strcmp(vk, "initializer_list") != 0) { + const CBMType* val_type = c_eval_expr_type(ctx, val); + const CBMType* val_base = c_simplify_type(ctx, val_type, false); + // If assigning an object of the same type -> copy/move constructor + const char* type_qn = type_to_qn(decl_type); + const char* val_qn = type_to_qn(val_base); + if (type_qn && val_qn && strcmp(type_qn, val_qn) == 0) { + const char* short_name = strrchr(type_qn, '.'); + short_name = short_name ? short_name + 1 : type_qn; + const char* ctor_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, short_name); + c_emit_resolved_call(ctx, ctor_qn, "lsp_copy_constructor", 0.85f); + } + } + } + } + } + } + } + + // --- Conversion operator: implicit bool/int conversion in conditions --- + if (ctx->cpp_mode && (strcmp(kind, "if_statement") == 0 || + strcmp(kind, "while_statement") == 0 || + strcmp(kind, "do_statement") == 0)) { + TSNode cond = ts_node_child_by_field_name(node, "condition", 9); + if (!ts_node_is_null(cond)) { + // If condition is a single expression of a custom type with operator bool + const CBMType* cond_type = c_eval_expr_type(ctx, cond); + const CBMType* base = c_simplify_type(ctx, cond_type, false); + if (base && base->kind != CBM_TYPE_BUILTIN && !cbm_type_is_unknown(base) && + base->kind != CBM_TYPE_POINTER) { + const char* type_qn = type_to_qn(base); + if (type_qn) { + const CBMRegisteredFunc* m = c_lookup_member(ctx, type_qn, "operator bool"); + if (m) { + c_emit_resolved_call(ctx, m->qualified_name, "lsp_conversion", 0.85f); + } + } + } + } + } + + // --- Designated initializer: {.field = value} --- + if (strcmp(kind, "initializer_pair") == 0 || strcmp(kind, "field_designator") == 0) { + // These appear inside initializer_list for aggregate init + // We don't emit CALLS for field access, but we could track which struct is being initialized + // This is primarily for completeness — no CALLS edge needed for field designators + } + +recurse:; + // Push scope for blocks and control structures + bool push_scope = (strcmp(kind, "compound_statement") == 0 || + strcmp(kind, "if_statement") == 0 || + strcmp(kind, "for_statement") == 0 || + strcmp(kind, "for_range_loop") == 0 || + strcmp(kind, "while_statement") == 0 || + strcmp(kind, "do_statement") == 0 || + strcmp(kind, "switch_statement") == 0 || + strcmp(kind, "catch_clause") == 0); + + if (push_scope) { + ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope); + } + + // Process catch clause parameter + if (strcmp(kind, "catch_clause") == 0) { + TSNode params = ts_node_child_by_field_name(node, "parameters", 10); + if (!ts_node_is_null(params)) { + uint32_t pnc = ts_node_named_child_count(params); + for (uint32_t pi = 0; pi < pnc; pi++) { + c_process_statement(ctx, ts_node_named_child(params, pi)); + } + } + } + + // Recurse into children + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + c_resolve_calls_in_node(ctx, ts_node_child(node, i)); + } + + if (push_scope) { + ctx->current_scope = cbm_scope_pop(ctx->current_scope); + } +} + +// ============================================================================ +// Process function: set enclosing QN, bind params, walk body +// ============================================================================ + +static void c_process_function(CLSPContext* ctx, TSNode func_node) { + TSNode decl = ts_node_child_by_field_name(func_node, "declarator", 10); + if (ts_node_is_null(decl)) return; + + // Extract function name from declarator chain + char* func_name = NULL; + const char* saved_class_qn = ctx->enclosing_class_qn; + TSNode params_node = (TSNode){0}; + + // Navigate declarator to find name and parameters + TSNode cur = decl; + for (int depth = 0; depth < 10 && !ts_node_is_null(cur); depth++) { + const char* dk = ts_node_type(cur); + + if (strcmp(dk, "function_declarator") == 0) { + TSNode fdecl = ts_node_child_by_field_name(cur, "declarator", 10); + params_node = ts_node_child_by_field_name(cur, "parameters", 10); + cur = fdecl; + continue; + } + if (strcmp(dk, "pointer_declarator") == 0 || strcmp(dk, "reference_declarator") == 0) { + if (ts_node_named_child_count(cur) > 0) + cur = ts_node_named_child(cur, ts_node_named_child_count(cur) - 1); + else break; + continue; + } + if (strcmp(dk, "qualified_identifier") == 0 || strcmp(dk, "scoped_identifier") == 0) { + func_name = c_node_text(ctx, cur); + // Check if this is a method (has Class:: prefix) + TSNode scope_node = ts_node_child_by_field_name(cur, "scope", 5); + if (!ts_node_is_null(scope_node)) { + char* scope_text = c_node_text(ctx, scope_node); + if (scope_text) { + const char* scope_qn = c_build_qn(ctx, scope_text); + // Try as a type for enclosing class + const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, scope_qn); + if (rt) { + ctx->enclosing_class_qn = scope_qn; + } else if (ctx->module_qn) { + const char* fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, scope_qn); + rt = cbm_registry_lookup_type(ctx->registry, fqn); + if (rt) ctx->enclosing_class_qn = fqn; + } + } + } + break; + } + if (strcmp(dk, "identifier") == 0) { + func_name = c_node_text(ctx, cur); + break; + } + if (strcmp(dk, "field_identifier") == 0) { + func_name = c_node_text(ctx, cur); + break; + } + // Destructor + if (strcmp(dk, "destructor_name") == 0) { + func_name = c_node_text(ctx, cur); + break; + } + break; + } + + if (!func_name || !func_name[0]) return; + + // Build enclosing function QN + const char* func_qn = c_build_qn(ctx, func_name); + if (ctx->module_qn && !strchr(func_qn, '.')) { + func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, func_qn); + } + ctx->enclosing_func_qn = func_qn; + + // If inside a template, attach type_param_names to the registered function + // so pending template calls can be resolved at call sites. + if (ctx->in_template && ctx->template_param_count > 0) { + // Find the registered function and set type_param_names + for (int ri = 0; ri < ((CBMTypeRegistry*)ctx->registry)->func_count; ri++) { + CBMRegisteredFunc* rf = &((CBMTypeRegistry*)ctx->registry)->funcs[ri]; + if (strcmp(rf->qualified_name, func_qn) == 0 && !rf->type_param_names) { + const char** tpn = (const char**)cbm_arena_alloc(ctx->arena, + (ctx->template_param_count + 1) * sizeof(const char*)); + for (int ti = 0; ti < ctx->template_param_count; ti++) + tpn[ti] = ctx->template_param_names[ti]; + tpn[ctx->template_param_count] = NULL; + rf->type_param_names = tpn; + break; + } + } + } + + // Push function scope + CBMScope* saved_scope = ctx->current_scope; + ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope); + + // Bind 'this' if in a method + if (ctx->enclosing_class_qn) { + cbm_scope_bind(ctx->current_scope, "this", + cbm_type_pointer(ctx->arena, + cbm_type_named(ctx->arena, ctx->enclosing_class_qn))); + } + + // Bind parameters + if (!ts_node_is_null(params_node)) { + uint32_t pnc = ts_node_named_child_count(params_node); + for (uint32_t i = 0; i < pnc; i++) { + TSNode param = ts_node_named_child(params_node, i); + if (!ts_node_is_null(param)) { + c_process_statement(ctx, param); + } + } + } + + // Walk function body + TSNode body = ts_node_child_by_field_name(func_node, "body", 4); + if (!ts_node_is_null(body)) { + c_resolve_calls_in_node(ctx, body); + } + + // Restore + ctx->current_scope = saved_scope; + ctx->enclosing_class_qn = saved_class_qn; +} + +// ============================================================================ +// Process namespace +// ============================================================================ + +// Process a top-level or nested declaration within a namespace/class body, +// handling template_declaration wrapping. +static void c_process_body_child(CLSPContext* ctx, TSNode child) { + if (ts_node_is_null(child)) return; + const char* ck = ts_node_type(child); + + if (strcmp(ck, "function_definition") == 0) { + c_process_function(ctx, child); + } else if (strcmp(ck, "namespace_definition") == 0) { + c_process_namespace(ctx, child); + } else if (strcmp(ck, "class_specifier") == 0 || strcmp(ck, "struct_specifier") == 0) { + c_process_class(ctx, child); + } else if (strcmp(ck, "concept_definition") == 0) { + // C++20 concept: concept Sortable = requires(T a) { a.sort(); }; + // No CALLS edges from concept definitions — they are constraints, not runtime calls. + // Skip processing to avoid false positive CALLS from requires-expressions. + return; + } else if (strcmp(ck, "template_declaration") == 0) { + // Save template state, parse params for defaults, set flag + bool saved_template = ctx->in_template; + const char** saved_tpn = ctx->template_param_names; + const CBMType** saved_tpd = ctx->template_param_defaults; + int saved_tpc = ctx->template_param_count; + + ctx->in_template = true; + c_parse_template_params(ctx, child); + + uint32_t tnc = ts_node_named_child_count(child); + for (uint32_t ti = 0; ti < tnc; ti++) { + TSNode inner = ts_node_named_child(child, ti); + c_process_body_child(ctx, inner); + } + + ctx->in_template = saved_template; + ctx->template_param_names = saved_tpn; + ctx->template_param_defaults = saved_tpd; + ctx->template_param_count = saved_tpc; + } else { + c_resolve_calls_in_node(ctx, child); + } +} + +static void c_process_namespace(CLSPContext* ctx, TSNode ns_node) { + const char* saved_ns = ctx->current_namespace; + + TSNode name_node = ts_node_child_by_field_name(ns_node, "name", 4); + if (!ts_node_is_null(name_node)) { + char* ns_name = c_node_text(ctx, name_node); + if (ns_name) { + if (saved_ns) { + ctx->current_namespace = cbm_arena_sprintf(ctx->arena, "%s.%s", saved_ns, ns_name); + } else if (ctx->module_qn) { + ctx->current_namespace = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, ns_name); + } else { + ctx->current_namespace = ns_name; + } + } + } + + TSNode body = ts_node_child_by_field_name(ns_node, "body", 4); + if (!ts_node_is_null(body)) { + uint32_t nc = ts_node_child_count(body); + for (uint32_t i = 0; i < nc; i++) { + c_process_body_child(ctx, ts_node_child(body, i)); + } + } + + ctx->current_namespace = saved_ns; +} + +// ============================================================================ +// Process class/struct: process method bodies +// ============================================================================ + +static void c_process_class(CLSPContext* ctx, TSNode class_node) { + const char* saved_class = ctx->enclosing_class_qn; + const char** saved_tpn = ctx->template_param_names; + const CBMType** saved_tpd = ctx->template_param_defaults; + int saved_tpc = ctx->template_param_count; + + char* class_name = NULL; + TSNode name_node = ts_node_child_by_field_name(class_node, "name", 4); + if (!ts_node_is_null(name_node)) { + class_name = c_node_text(ctx, name_node); + if (class_name) { + const char* class_qn; + if (ctx->current_namespace) { + class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->current_namespace, class_name); + } else if (ctx->module_qn) { + class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, class_name); + } else { + class_qn = class_name; + } + ctx->enclosing_class_qn = class_qn; + } + } + + // CRTP detection: class Derived : Base + // When a base class template argument matches the class name, + // bind that template parameter to the derived class type. + if (class_name && ctx->enclosing_class_qn) { + // Walk base_class_clause children looking for template base classes + uint32_t cnc = ts_node_named_child_count(class_node); + for (uint32_t ci = 0; ci < cnc; ci++) { + TSNode child = ts_node_named_child(class_node, ci); + if (ts_node_is_null(child)) continue; + const char* ck = ts_node_type(child); + if (strcmp(ck, "base_class_clause") != 0) continue; + // Iterate base specifiers + uint32_t bnc = ts_node_named_child_count(child); + for (uint32_t bi = 0; bi < bnc; bi++) { + TSNode base_spec = ts_node_named_child(child, bi); + if (ts_node_is_null(base_spec)) continue; + // Look for template_type as base: Base + TSNode type_node = base_spec; + if (strcmp(ts_node_type(base_spec), "access_specifier") == 0) continue; + // The base type might be directly a template_type or wrapped + if (strcmp(ts_node_type(type_node), "template_type") != 0) { + // Try walking children for template_type + uint32_t snc = ts_node_named_child_count(type_node); + bool found = false; + for (uint32_t si = 0; si < snc; si++) { + TSNode sc = ts_node_named_child(type_node, si); + if (strcmp(ts_node_type(sc), "template_type") == 0) { + type_node = sc; + found = true; + break; + } + } + if (!found) continue; + } + // type_node is template_type: check arguments for CRTP + TSNode targs = ts_node_child_by_field_name(type_node, "arguments", 9); + if (ts_node_is_null(targs)) continue; + // Check each template argument for CRTP pattern + uint32_t tanc = ts_node_named_child_count(targs); + for (uint32_t ti = 0; ti < tanc; ti++) { + TSNode ta = ts_node_named_child(targs, ti); + if (ts_node_is_null(ta)) continue; + TSNode ta_inner = ta; + // type_descriptor wraps the actual type + if (strcmp(ts_node_type(ta), "type_descriptor") == 0 && + ts_node_named_child_count(ta) > 0) { + ta_inner = ts_node_named_child(ta, 0); + } + char* arg_text = c_node_text(ctx, ta_inner); + if (arg_text && strcmp(arg_text, class_name) == 0) { + // CRTP detected! Map template param at position ti to derived class + // For now, bind using positional names (T0, T1, ...) + // Also check existing template params + if (ctx->template_param_names) { + for (int pi = 0; pi < ctx->template_param_count; pi++) { + if (ctx->template_param_names[pi] && + ctx->template_param_defaults && !ctx->template_param_defaults[pi]) { + // Bind unbound template param to derived class + // Only bind the first unbound one at position ti + if (pi == (int)ti) { + ctx->template_param_defaults[pi] = + cbm_type_named(ctx->arena, ctx->enclosing_class_qn); + break; + } + } + } + } + break; // found CRTP for this base + } + } + } + break; // only one base_class_clause + } + } + + TSNode body = ts_node_child_by_field_name(class_node, "body", 4); + if (!ts_node_is_null(body)) { + uint32_t nc = ts_node_child_count(body); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(body, i); + if (ts_node_is_null(child) || !ts_node_is_named(child)) continue; + c_process_body_child(ctx, child); + } + } + + ctx->enclosing_class_qn = saved_class; + ctx->template_param_names = saved_tpn; + ctx->template_param_defaults = saved_tpd; + ctx->template_param_count = saved_tpc; +} + +// Need forward declaration since c_process_namespace and c_process_class reference each other +// Already defined above via forward declaration pattern — both reference c_resolve_calls_in_node + +// ============================================================================ +// Process file: top-level walk +// ============================================================================ + +void c_lsp_process_file(CLSPContext* ctx, TSNode root) { + if (ts_node_is_null(root)) return; + + uint32_t nc = ts_node_child_count(root); + + // Pass 1: process using declarations and global variables + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(root, i); + if (ts_node_is_null(child)) continue; + const char* ck = ts_node_type(child); + + if (strcmp(ck, "using_declaration") == 0 || + strcmp(ck, "alias_declaration") == 0 || + strcmp(ck, "type_definition") == 0 || + strcmp(ck, "namespace_alias_definition") == 0) { + c_process_statement(ctx, child); + } else if (strcmp(ck, "declaration") == 0) { + c_process_statement(ctx, child); + } + } + + // Pass 2: process functions, namespaces, classes, templates + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(root, i); + if (ts_node_is_null(child)) continue; + c_process_body_child(ctx, child); + } +} + +// ============================================================================ +// Parse C/C++ return type text into CBMType (for cross-file defs) +// ============================================================================ + +static const CBMType* c_parse_return_type_text(CBMArena* a, const char* text, const char* module_qn) { + if (!text || !text[0]) return cbm_type_unknown(); + + // Skip const/volatile qualifiers + while (strncmp(text, "const ", 6) == 0) text += 6; + while (strncmp(text, "volatile ", 9) == 0) text += 9; + + // Pointer + size_t len = strlen(text); + if (len > 0 && text[len - 1] == '*') { + char* inner = cbm_arena_strndup(a, text, len - 1); + // Trim trailing space + size_t ilen = strlen(inner); + while (ilen > 0 && inner[ilen - 1] == ' ') inner[--ilen] = '\0'; + return cbm_type_pointer(a, c_parse_return_type_text(a, inner, module_qn)); + } + + // Reference + if (len > 0 && text[len - 1] == '&') { + char* inner = cbm_arena_strndup(a, text, len - 1); + size_t ilen = strlen(inner); + while (ilen > 0 && inner[ilen - 1] == ' ') inner[--ilen] = '\0'; + // Check for rvalue ref + if (ilen > 0 && inner[ilen - 1] == '&') { + inner[ilen - 1] = '\0'; + while (ilen > 1 && inner[ilen - 2] == ' ') inner[--ilen - 1] = '\0'; + return cbm_type_rvalue_ref(a, c_parse_return_type_text(a, inner, module_qn)); + } + return cbm_type_reference(a, c_parse_return_type_text(a, inner, module_qn)); + } + + // Builtins + if (is_c_builtin_type(text)) return cbm_type_builtin(a, text); + + // Template type parameters: single uppercase letter or T1-T9 + // Recognized as TYPE_PARAM so template substitution works on method return types. + if (len == 1 && text[0] >= 'A' && text[0] <= 'Z') { + return cbm_type_type_param(a, text); + } + if (len == 2 && text[0] == 'T' && text[1] >= '1' && text[1] <= '9') { + return cbm_type_type_param(a, text); + } + + // Qualified name (has ::) + if (strstr(text, "::")) { + // Replace :: with . + char* qn = cbm_arena_strdup(a, text); + for (char* p = qn; *p; p++) { + if (p[0] == ':' && p[1] == ':') { p[0] = '.'; memmove(p + 1, p + 2, strlen(p + 2) + 1); } + } + return cbm_type_named(a, qn); + } + + // Named type: qualify with module + if (module_qn && module_qn[0]) { + return cbm_type_named(a, cbm_arena_sprintf(a, "%s.%s", module_qn, text)); + } + return cbm_type_named(a, text); +} + +// ============================================================================ +// Entry point: single-file LSP +// ============================================================================ + +void cbm_run_c_lsp(CBMArena* arena, CBMFileResult* result, + const char* source, int source_len, TSNode root, bool cpp_mode) { + + CBMTypeRegistry reg; + cbm_registry_init(®, arena); + + // Register stdlib + cbm_c_stdlib_register(®, arena); + if (cpp_mode) cbm_cpp_stdlib_register(®, arena); + + const char* module_qn = result->module_qn; + + // Register file's own definitions + for (int i = 0; i < result->defs.count; i++) { + CBMDefinition* d = &result->defs.items[i]; + if (!d->qualified_name || !d->name) continue; + + if (d->label && (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Type") == 0)) { + CBMRegisteredType rt; + memset(&rt, 0, sizeof(rt)); + rt.qualified_name = d->qualified_name; + rt.short_name = d->name; + + // Base classes + if (d->base_classes) { + int bc_count = 0; + while (d->base_classes[bc_count]) bc_count++; + if (bc_count > 0) { + const char** embedded = (const char**)cbm_arena_alloc(arena, (bc_count + 1) * sizeof(const char*)); + for (int j = 0; j < bc_count; j++) { + const char* bc = d->base_classes[j]; + // Check if already qualified + if (strchr(bc, '.') || strstr(bc, "::")) { + embedded[j] = bc; + } else { + embedded[j] = cbm_arena_sprintf(arena, "%s.%s", module_qn, bc); + } + } + embedded[bc_count] = NULL; + rt.embedded_types = embedded; + } + } + cbm_registry_add_type(®, rt); + } + + if (d->label && (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0)) { + CBMRegisteredFunc rf; + memset(&rf, 0, sizeof(rf)); + rf.qualified_name = d->qualified_name; + rf.short_name = d->name; + + // Build return type + const CBMType** ret_types = NULL; + if (d->return_types) { + int count = 0; + while (d->return_types[count]) count++; + if (count > 0) { + ret_types = (const CBMType**)cbm_arena_alloc(arena, (count + 1) * sizeof(const CBMType*)); + for (int j = 0; j < count; j++) + ret_types[j] = c_parse_return_type_text(arena, d->return_types[j], module_qn); + ret_types[count] = NULL; + } + } else if (d->return_type && d->return_type[0]) { + ret_types = (const CBMType**)cbm_arena_alloc(arena, 2 * sizeof(const CBMType*)); + ret_types[0] = c_parse_return_type_text(arena, d->return_type, module_qn); + ret_types[1] = NULL; + } + + // Build param types + const CBMType** param_types_arr = NULL; + if (d->param_types) { + int count = 0; + while (d->param_types[count]) count++; + if (count > 0) { + param_types_arr = (const CBMType**)cbm_arena_alloc(arena, (count + 1) * sizeof(const CBMType*)); + for (int j = 0; j < count; j++) + param_types_arr[j] = c_parse_return_type_text(arena, d->param_types[j], module_qn); + param_types_arr[count] = NULL; + } + } + + rf.signature = cbm_type_func(arena, d->param_names, param_types_arr, ret_types); + + // Method receiver + if (strcmp(d->label, "Method") == 0 && d->parent_class) { + rf.receiver_type = d->parent_class; + // Auto-create type if needed + if (!cbm_registry_lookup_type(®, d->parent_class)) { + CBMRegisteredType auto_type; + memset(&auto_type, 0, sizeof(auto_type)); + auto_type.qualified_name = d->parent_class; + // Extract short name + const char* dot = strrchr(d->parent_class, '.'); + auto_type.short_name = dot ? dot + 1 : d->parent_class; + cbm_registry_add_type(®, auto_type); + } + } + + cbm_registry_add_func(®, rf); + } + } + + // Initialize context and run + CLSPContext ctx; + c_lsp_init(&ctx, arena, source, source_len, ®, module_qn, cpp_mode, &result->resolved_calls); + + c_lsp_process_file(&ctx, root); +} + +// ============================================================================ +// Entry point: cross-file LSP +// ============================================================================ + +void cbm_run_c_lsp_cross( + CBMArena* arena, + const char* source, int source_len, + const char* module_qn, + bool cpp_mode, + CBMLSPDef* defs, int def_count, + const char** include_paths, const char** include_ns_qns, int include_count, + CBMResolvedCallArray* out) { + + if (!source || source_len == 0 || !out) return; + + CBMTypeRegistry reg; + cbm_registry_init(®, arena); + + // Register stdlib + cbm_c_stdlib_register(®, arena); + if (cpp_mode) cbm_cpp_stdlib_register(®, arena); + + // Register all defs + for (int i = 0; i < def_count; i++) { + CBMLSPDef* d = &defs[i]; + if (!d->qualified_name || !d->short_name) continue; + + if (d->label && (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Type") == 0 || + strcmp(d->label, "Interface") == 0)) { + CBMRegisteredType rt; + memset(&rt, 0, sizeof(rt)); + rt.qualified_name = cbm_arena_strdup(arena, d->qualified_name); + rt.short_name = cbm_arena_strdup(arena, d->short_name); + rt.is_interface = d->is_interface; + + // Embedded/base types + if (d->embedded_types) { + // Parse "|"-separated list + const char* src = d->embedded_types; + const char* embeds[32]; + int embed_count = 0; + while (*src && embed_count < 31) { + const char* sep = strchr(src, '|'); + if (sep) { + embeds[embed_count++] = cbm_arena_strndup(arena, src, sep - src); + src = sep + 1; + } else { + embeds[embed_count++] = cbm_arena_strdup(arena, src); + break; + } + } + if (embed_count > 0) { + const char** arr = (const char**)cbm_arena_alloc(arena, (embed_count + 1) * sizeof(const char*)); + for (int j = 0; j < embed_count; j++) arr[j] = embeds[j]; + arr[embed_count] = NULL; + rt.embedded_types = arr; + } + } + + // Field defs + if (d->field_defs) { + const char* fsrc = d->field_defs; + const char* fnames[64]; + const CBMType* ftypes[64]; + int fcount = 0; + while (*fsrc && fcount < 63) { + const char* sep = strchr(fsrc, '|'); + const char* end = sep ? sep : fsrc + strlen(fsrc); + char* pair = cbm_arena_strndup(arena, fsrc, end - fsrc); + char* colon = strchr(pair, ':'); + if (colon) { + *colon = '\0'; + fnames[fcount] = pair; + ftypes[fcount] = c_parse_return_type_text(arena, colon + 1, + d->def_module_qn ? d->def_module_qn : module_qn); + fcount++; + } + if (!sep) break; + fsrc = sep + 1; + } + if (fcount > 0) { + const char** fnarr = (const char**)cbm_arena_alloc(arena, (fcount + 1) * sizeof(const char*)); + const CBMType** ftarr = (const CBMType**)cbm_arena_alloc(arena, (fcount + 1) * sizeof(const CBMType*)); + for (int j = 0; j < fcount; j++) { fnarr[j] = fnames[j]; ftarr[j] = ftypes[j]; } + fnarr[fcount] = NULL; + ftarr[fcount] = NULL; + rt.field_names = fnarr; + rt.field_types = ftarr; + } + } + + cbm_registry_add_type(®, rt); + } + + if (d->label && (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0)) { + CBMRegisteredFunc rf; + memset(&rf, 0, sizeof(rf)); + rf.qualified_name = cbm_arena_strdup(arena, d->qualified_name); + rf.short_name = cbm_arena_strdup(arena, d->short_name); + + const char* def_module = d->def_module_qn ? d->def_module_qn : module_qn; + + // Return types + if (d->return_types) { + // Parse "|"-separated + const char* rsrc = d->return_types; + const CBMType* rets[16]; + int rcount = 0; + while (*rsrc && rcount < 15) { + const char* sep = strchr(rsrc, '|'); + const char* end = sep ? sep : rsrc + strlen(rsrc); + char* rt_text = cbm_arena_strndup(arena, rsrc, end - rsrc); + rets[rcount++] = c_parse_return_type_text(arena, rt_text, def_module); + if (!sep) break; + rsrc = sep + 1; + } + if (rcount > 0) { + const CBMType** rarr = (const CBMType**)cbm_arena_alloc(arena, (rcount + 1) * sizeof(const CBMType*)); + for (int j = 0; j < rcount; j++) rarr[j] = rets[j]; + rarr[rcount] = NULL; + rf.signature = cbm_type_func(arena, NULL, NULL, rarr); + } + } + if (!rf.signature) rf.signature = cbm_type_func(arena, NULL, NULL, NULL); + + if (d->receiver_type) { + rf.receiver_type = cbm_arena_strdup(arena, d->receiver_type); + } + + cbm_registry_add_func(®, rf); + } + } + + // Parse the source with tree-sitter + TSParser* parser = ts_parser_new(); + if (!parser) return; + + const TSLanguage* ts_lang = cpp_mode ? + tree_sitter_cpp() : tree_sitter_c(); + ts_parser_set_language(parser, ts_lang); + + TSTree* tree = ts_parser_parse_string(parser, NULL, source, source_len); + ts_parser_delete(parser); + if (!tree) return; + + TSNode root = ts_tree_root_node(tree); + + // Initialize context and run + CLSPContext ctx; + c_lsp_init(&ctx, arena, source, source_len, ®, module_qn, cpp_mode, out); + + // Add include mappings + for (int i = 0; i < include_count; i++) { + c_lsp_add_include(&ctx, include_paths[i], include_ns_qns[i]); + } + + c_lsp_process_file(&ctx, root); + + ts_tree_delete(tree); +} diff --git a/internal/cbm/lsp/c_lsp.h b/internal/cbm/lsp/c_lsp.h new file mode 100644 index 000000000..5e8531e13 --- /dev/null +++ b/internal/cbm/lsp/c_lsp.h @@ -0,0 +1,121 @@ +#ifndef CBM_LSP_C_LSP_H +#define CBM_LSP_C_LSP_H + +#include "type_rep.h" +#include "scope.h" +#include "type_registry.h" +#include "../cbm.h" +#include "go_lsp.h" // for CBMLSPDef, CBMResolvedCallArray + +// CLSPContext holds state for C/C++ expression type evaluation within a file. +typedef struct { + CBMArena* arena; + const char* source; + int source_len; + const CBMTypeRegistry* registry; + CBMScope* current_scope; + + // Include map: header_path -> namespace QN prefix + const char** include_paths; + const char** include_ns_qns; + int include_count; + + // Namespace state + const char* current_namespace; // current namespace QN (e.g., "proj.ns1.ns2") + + // Using namespace directives + const char** using_namespaces; + int using_ns_count; + int using_ns_cap; + + // Using declarations: specific names imported into scope + const char** using_decl_names; + const char** using_decl_qns; + int using_decl_count; + int using_decl_cap; + + // Namespace aliases: short -> full QN + const char** ns_alias_names; + const char** ns_alias_qns; + int ns_alias_count; + int ns_alias_cap; + + // Current context + const char* enclosing_func_qn; + const char* enclosing_class_qn; // for implicit `this` resolution + const char* module_qn; + + // Output + CBMResolvedCallArray* resolved_calls; + + // Function pointer targets: var_name -> target function QN + const char** fp_var_names; + const char** fp_target_qns; + int fp_count; + int fp_cap; + + // Template parameter defaults for current template scope + const char** template_param_names; // e.g., ["T", "U"] + const CBMType** template_param_defaults; // e.g., [int_type, NULL] + int template_param_count; + + // Pending template calls: member calls on TYPE_PARAM inside template functions. + // At call sites with known arg types, these are resolved retroactively. + struct { + const char* func_qn; // enclosing template function + const char* type_param; // e.g., "T" + const char* method_name; // e.g., "draw" + int arg_count; + } *pending_template_calls; + int pending_tc_count; + int pending_tc_cap; + + // Flags + bool cpp_mode; // C++ features enabled + bool in_template; // currently inside template declaration + bool debug; +} CLSPContext; + +// --- API --- + +void c_lsp_init(CLSPContext* ctx, CBMArena* arena, const char* source, int source_len, + const CBMTypeRegistry* registry, const char* module_qn, bool cpp_mode, + CBMResolvedCallArray* out); + +void c_lsp_add_include(CLSPContext* ctx, const char* header_path, const char* ns_qn); + +void c_lsp_process_file(CLSPContext* ctx, TSNode root); + +const CBMType* c_eval_expr_type(CLSPContext* ctx, TSNode node); +const CBMType* c_parse_type_node(CLSPContext* ctx, TSNode node); +void c_process_statement(CLSPContext* ctx, TSNode node); + +// Look up a member (method/field) on a type, traversing base classes. +const CBMRegisteredFunc* c_lookup_member(CLSPContext* ctx, const char* type_qn, const char* member_name); + +// Type simplification: unwrap refs, aliases, pointers (like clangd simplifyType). +const CBMType* c_simplify_type(CLSPContext* ctx, const CBMType* t, bool unwrap_pointer); + +// --- Entry points --- + +// Single-file LSP: build registry from file defs + stdlib, run resolution. +void cbm_run_c_lsp(CBMArena* arena, CBMFileResult* result, + const char* source, int source_len, TSNode root, bool cpp_mode); + +// Cross-file LSP: build registry from defs + stdlib, re-parse and resolve. +void cbm_run_c_lsp_cross( + CBMArena* arena, + const char* source, int source_len, + const char* module_qn, + bool cpp_mode, + CBMLSPDef* defs, int def_count, + const char** include_paths, const char** include_ns_qns, int include_count, + CBMResolvedCallArray* out); + +// Register C stdlib types and functions into a registry. +void cbm_c_stdlib_register(CBMTypeRegistry* reg, CBMArena* arena); + +// Register C++ stdlib types and functions into a registry. +void cbm_cpp_stdlib_register(CBMTypeRegistry* reg, CBMArena* arena); + +#endif // CBM_LSP_C_LSP_H diff --git a/internal/cbm/lsp/generated/c_stdlib_data.c b/internal/cbm/lsp/generated/c_stdlib_data.c new file mode 100644 index 000000000..ae84adf7f --- /dev/null +++ b/internal/cbm/lsp/generated/c_stdlib_data.c @@ -0,0 +1,131 @@ +// C stdlib type information for C/C++ LSP type resolver. + +#include "../type_rep.h" +#include "../type_registry.h" +#include + +// Helper macros for concise registration +#define REG_FUNC(qn, short, ret_type) do { \ + memset(&rf, 0, sizeof(rf)); \ + rf.qualified_name = (qn); \ + rf.short_name = (short); \ + rf.signature = cbm_type_func(arena, NULL, NULL, (const CBMType*[]){(ret_type), NULL}); \ + cbm_registry_add_func(reg, rf); \ +} while(0) + +#define REG_TYPE(qn, short) do { \ + memset(&rt, 0, sizeof(rt)); \ + rt.qualified_name = (qn); \ + rt.short_name = (short); \ + cbm_registry_add_type(reg, rt); \ +} while(0) + +void cbm_c_stdlib_register(CBMTypeRegistry* reg, CBMArena* arena) { + CBMRegisteredFunc rf; + CBMRegisteredType rt; + + const CBMType* t_int = cbm_type_builtin(arena, "int"); + const CBMType* t_size_t = cbm_type_builtin(arena, "size_t"); + const CBMType* t_double = cbm_type_builtin(arena, "double"); + const CBMType* t_void = cbm_type_builtin(arena, "void"); + const CBMType* t_char_ptr = cbm_type_pointer(arena, cbm_type_builtin(arena, "char")); + const CBMType* t_void_ptr = cbm_type_pointer(arena, t_void); + + // FILE type + REG_TYPE("FILE", "FILE"); + const CBMType* t_file_ptr = cbm_type_pointer(arena, cbm_type_named(arena, "FILE")); + + // stdio.h + REG_FUNC("fopen", "fopen", t_file_ptr); + REG_FUNC("fclose", "fclose", t_int); + REG_FUNC("fprintf", "fprintf", t_int); + REG_FUNC("fread", "fread", t_size_t); + REG_FUNC("fwrite", "fwrite", t_size_t); + REG_FUNC("printf", "printf", t_int); + REG_FUNC("scanf", "scanf", t_int); + REG_FUNC("fgets", "fgets", t_char_ptr); + REG_FUNC("fputs", "fputs", t_int); + REG_FUNC("fseek", "fseek", t_int); + REG_FUNC("ftell", "ftell", cbm_type_builtin(arena, "long")); + REG_FUNC("rewind", "rewind", t_void); + REG_FUNC("feof", "feof", t_int); + REG_FUNC("ferror", "ferror", t_int); + REG_FUNC("snprintf", "snprintf", t_int); + REG_FUNC("sprintf", "sprintf", t_int); + REG_FUNC("sscanf", "sscanf", t_int); + REG_FUNC("getchar", "getchar", t_int); + REG_FUNC("putchar", "putchar", t_int); + REG_FUNC("puts", "puts", t_int); + REG_FUNC("perror", "perror", t_void); + + // stdlib.h + REG_FUNC("malloc", "malloc", t_void_ptr); + REG_FUNC("calloc", "calloc", t_void_ptr); + REG_FUNC("realloc", "realloc", t_void_ptr); + REG_FUNC("free", "free", t_void); + REG_FUNC("atoi", "atoi", t_int); + REG_FUNC("atof", "atof", t_double); + REG_FUNC("atol", "atol", cbm_type_builtin(arena, "long")); + REG_FUNC("strtol", "strtol", cbm_type_builtin(arena, "long")); + REG_FUNC("strtod", "strtod", t_double); + REG_FUNC("exit", "exit", t_void); + REG_FUNC("abort", "abort", t_void); + REG_FUNC("abs", "abs", t_int); + REG_FUNC("rand", "rand", t_int); + REG_FUNC("srand", "srand", t_void); + REG_FUNC("qsort", "qsort", t_void); + REG_FUNC("bsearch", "bsearch", t_void_ptr); + REG_FUNC("getenv", "getenv", t_char_ptr); + REG_FUNC("system", "system", t_int); + + // string.h + REG_FUNC("strlen", "strlen", t_size_t); + REG_FUNC("strcmp", "strcmp", t_int); + REG_FUNC("strncmp", "strncmp", t_int); + REG_FUNC("strcpy", "strcpy", t_char_ptr); + REG_FUNC("strncpy", "strncpy", t_char_ptr); + REG_FUNC("strcat", "strcat", t_char_ptr); + REG_FUNC("strncat", "strncat", t_char_ptr); + REG_FUNC("strchr", "strchr", t_char_ptr); + REG_FUNC("strrchr", "strrchr", t_char_ptr); + REG_FUNC("strstr", "strstr", t_char_ptr); + REG_FUNC("strtok", "strtok", t_char_ptr); + REG_FUNC("memcpy", "memcpy", t_void_ptr); + REG_FUNC("memmove", "memmove", t_void_ptr); + REG_FUNC("memset", "memset", t_void_ptr); + REG_FUNC("memcmp", "memcmp", t_int); + + // ctype.h + REG_FUNC("isalpha", "isalpha", t_int); + REG_FUNC("isdigit", "isdigit", t_int); + REG_FUNC("isalnum", "isalnum", t_int); + REG_FUNC("isspace", "isspace", t_int); + REG_FUNC("isupper", "isupper", t_int); + REG_FUNC("islower", "islower", t_int); + REG_FUNC("toupper", "toupper", t_int); + REG_FUNC("tolower", "tolower", t_int); + + // math.h + REG_FUNC("sqrt", "sqrt", t_double); + REG_FUNC("pow", "pow", t_double); + REG_FUNC("sin", "sin", t_double); + REG_FUNC("cos", "cos", t_double); + REG_FUNC("tan", "tan", t_double); + REG_FUNC("exp", "exp", t_double); + REG_FUNC("log", "log", t_double); + REG_FUNC("log10", "log10", t_double); + REG_FUNC("fabs", "fabs", t_double); + REG_FUNC("ceil", "ceil", t_double); + REG_FUNC("floor", "floor", t_double); + REG_FUNC("round", "round", t_double); + + // assert.h / signal.h / time.h basics + REG_FUNC("assert", "assert", t_void); + REG_FUNC("signal", "signal", t_void); + REG_FUNC("time", "time", cbm_type_builtin(arena, "time_t")); + REG_FUNC("clock", "clock", cbm_type_builtin(arena, "clock_t")); + REG_FUNC("difftime", "difftime", t_double); +} + +#undef REG_FUNC +#undef REG_TYPE diff --git a/internal/cbm/lsp/generated/cpp_stdlib_data.c b/internal/cbm/lsp/generated/cpp_stdlib_data.c new file mode 100644 index 000000000..2c79ff5ce --- /dev/null +++ b/internal/cbm/lsp/generated/cpp_stdlib_data.c @@ -0,0 +1,945 @@ +// C++ stdlib type information for C/C++ LSP type resolver. + +#include "../type_rep.h" +#include "../type_registry.h" +#include + +// Helper: register a method on a type +static void reg_method(CBMTypeRegistry* reg, CBMArena* arena, + const char* recv_qn, const char* method_name, const CBMType* ret_type) { + CBMRegisteredFunc rf; + memset(&rf, 0, sizeof(rf)); + // Build QN: recv_qn.method_name + size_t rlen = strlen(recv_qn); + size_t mlen = strlen(method_name); + char* qn = (char*)cbm_arena_alloc(arena, rlen + 1 + mlen + 1); + if (!qn) return; + memcpy(qn, recv_qn, rlen); + qn[rlen] = '.'; + memcpy(qn + rlen + 1, method_name, mlen); + qn[rlen + 1 + mlen] = '\0'; + + rf.qualified_name = qn; + rf.short_name = method_name; + rf.receiver_type = recv_qn; + rf.signature = cbm_type_func(arena, NULL, NULL, + ret_type ? (const CBMType*[]){ret_type, NULL} : NULL); + cbm_registry_add_func(reg, rf); +} + +// Helper: register a type +static void reg_type(CBMTypeRegistry* reg, const char* qn, const char* short_name) { + CBMRegisteredType rt; + memset(&rt, 0, sizeof(rt)); + rt.qualified_name = qn; + rt.short_name = short_name; + cbm_registry_add_type(reg, rt); +} + +// Helper: register a type with field names +static void reg_type_with_fields(CBMTypeRegistry* reg, CBMArena* arena, + const char* qn, const char* short_name, + const char** fnames, const CBMType** ftypes) { + CBMRegisteredType rt; + memset(&rt, 0, sizeof(rt)); + rt.qualified_name = qn; + rt.short_name = short_name; + rt.field_names = fnames; + rt.field_types = ftypes; + cbm_registry_add_type(reg, rt); +} + +// Helper: register a free function +static void reg_func(CBMTypeRegistry* reg, CBMArena* arena, + const char* qn, const char* short_name, const CBMType* ret_type) { + CBMRegisteredFunc rf; + memset(&rf, 0, sizeof(rf)); + rf.qualified_name = qn; + rf.short_name = short_name; + rf.signature = cbm_type_func(arena, NULL, NULL, + ret_type ? (const CBMType*[]){ret_type, NULL} : NULL); + cbm_registry_add_func(reg, rf); +} + +void cbm_cpp_stdlib_register(CBMTypeRegistry* reg, CBMArena* arena) { + const CBMType* t_void = cbm_type_builtin(arena, "void"); + const CBMType* t_void_ptr = cbm_type_pointer(arena, t_void); + const CBMType* t_bool = cbm_type_builtin(arena, "bool"); + const CBMType* t_int = cbm_type_builtin(arena, "int"); + const CBMType* t_size_t = cbm_type_builtin(arena, "size_t"); + const CBMType* t_long = cbm_type_builtin(arena, "long"); + const CBMType* t_char = cbm_type_builtin(arena, "char"); + const CBMType* t_char_ptr = cbm_type_pointer(arena, cbm_type_builtin(arena, "const char")); + + // ========================================================================= + // std.string + // ========================================================================= + const char* string_qn = "std.string"; + reg_type(reg, string_qn, "string"); + const CBMType* t_string = cbm_type_named(arena, string_qn); + const CBMType* t_string_ref = cbm_type_reference(arena, t_string); + + reg_method(reg, arena, string_qn, "c_str", t_char_ptr); + reg_method(reg, arena, string_qn, "data", t_char_ptr); + reg_method(reg, arena, string_qn, "size", t_size_t); + reg_method(reg, arena, string_qn, "length", t_size_t); + reg_method(reg, arena, string_qn, "capacity", t_size_t); + reg_method(reg, arena, string_qn, "max_size", t_size_t); + reg_method(reg, arena, string_qn, "empty", t_bool); + reg_method(reg, arena, string_qn, "clear", t_void); + reg_method(reg, arena, string_qn, "substr", t_string); + reg_method(reg, arena, string_qn, "find", t_size_t); + reg_method(reg, arena, string_qn, "rfind", t_size_t); + reg_method(reg, arena, string_qn, "compare", t_int); + reg_method(reg, arena, string_qn, "append", t_string_ref); + reg_method(reg, arena, string_qn, "insert", t_string_ref); + reg_method(reg, arena, string_qn, "erase", t_string_ref); + reg_method(reg, arena, string_qn, "replace", t_string_ref); + reg_method(reg, arena, string_qn, "push_back", t_void); + reg_method(reg, arena, string_qn, "pop_back", t_void); + reg_method(reg, arena, string_qn, "front", cbm_type_reference(arena, t_char)); + reg_method(reg, arena, string_qn, "back", cbm_type_reference(arena, t_char)); + reg_method(reg, arena, string_qn, "at", cbm_type_reference(arena, t_char)); + reg_method(reg, arena, string_qn, "operator[]", cbm_type_reference(arena, t_char)); + reg_method(reg, arena, string_qn, "begin", cbm_type_named(arena, "std.string.iterator")); + reg_method(reg, arena, string_qn, "end", cbm_type_named(arena, "std.string.iterator")); + reg_method(reg, arena, string_qn, "resize", t_void); + reg_method(reg, arena, string_qn, "reserve", t_void); + reg_method(reg, arena, string_qn, "starts_with", t_bool); + reg_method(reg, arena, string_qn, "ends_with", t_bool); + reg_method(reg, arena, string_qn, "contains", t_bool); + reg_method(reg, arena, string_qn, "operator+", t_string); + reg_method(reg, arena, string_qn, "operator+=", t_string_ref); + // Also register as std.basic_string alias + { + CBMRegisteredType rt; + memset(&rt, 0, sizeof(rt)); + rt.qualified_name = "std.basic_string"; + rt.short_name = "basic_string"; + rt.alias_of = string_qn; + cbm_registry_add_type(reg, rt); + } + + // ========================================================================= + // std.string_view + // ========================================================================= + const char* sv_qn = "std.string_view"; + reg_type(reg, sv_qn, "string_view"); + reg_method(reg, arena, sv_qn, "data", t_char_ptr); + reg_method(reg, arena, sv_qn, "size", t_size_t); + reg_method(reg, arena, sv_qn, "length", t_size_t); + reg_method(reg, arena, sv_qn, "empty", t_bool); + reg_method(reg, arena, sv_qn, "substr", cbm_type_named(arena, sv_qn)); + reg_method(reg, arena, sv_qn, "find", t_size_t); + reg_method(reg, arena, sv_qn, "starts_with", t_bool); + reg_method(reg, arena, sv_qn, "ends_with", t_bool); + reg_method(reg, arena, sv_qn, "operator[]", t_char); + + // ========================================================================= + // std.vector — T is first template arg + // ========================================================================= + const char* vec_qn = "std.vector"; + reg_type(reg, vec_qn, "vector"); + const CBMType* t_T = cbm_type_type_param(arena, "T"); + const CBMType* t_T_ref = cbm_type_reference(arena, t_T); + const CBMType* t_T_ptr = cbm_type_pointer(arena, t_T); + + reg_method(reg, arena, vec_qn, "push_back", t_void); + reg_method(reg, arena, vec_qn, "emplace_back", t_void); + reg_method(reg, arena, vec_qn, "pop_back", t_void); + reg_method(reg, arena, vec_qn, "size", t_size_t); + reg_method(reg, arena, vec_qn, "capacity", t_size_t); + reg_method(reg, arena, vec_qn, "empty", t_bool); + reg_method(reg, arena, vec_qn, "clear", t_void); + reg_method(reg, arena, vec_qn, "resize", t_void); + reg_method(reg, arena, vec_qn, "reserve", t_void); + reg_method(reg, arena, vec_qn, "shrink_to_fit", t_void); + reg_method(reg, arena, vec_qn, "front", t_T_ref); + reg_method(reg, arena, vec_qn, "back", t_T_ref); + reg_method(reg, arena, vec_qn, "at", t_T_ref); + reg_method(reg, arena, vec_qn, "data", t_T_ptr); + reg_method(reg, arena, vec_qn, "operator[]", t_T_ref); + reg_method(reg, arena, vec_qn, "begin", cbm_type_named(arena, "std.vector.iterator")); + reg_method(reg, arena, vec_qn, "end", cbm_type_named(arena, "std.vector.iterator")); + reg_method(reg, arena, vec_qn, "erase", cbm_type_named(arena, "std.vector.iterator")); + reg_method(reg, arena, vec_qn, "insert", cbm_type_named(arena, "std.vector.iterator")); + + // ========================================================================= + // std.map + // ========================================================================= + const char* map_qn = "std.map"; + reg_type(reg, map_qn, "map"); + const CBMType* t_V = cbm_type_type_param(arena, "V"); + const CBMType* t_V_ref = cbm_type_reference(arena, t_V); + + reg_method(reg, arena, map_qn, "operator[]", t_V_ref); + reg_method(reg, arena, map_qn, "at", t_V_ref); + reg_method(reg, arena, map_qn, "find", cbm_type_named(arena, "std.map.iterator")); + reg_method(reg, arena, map_qn, "insert", cbm_type_named(arena, "std.pair")); + reg_method(reg, arena, map_qn, "emplace", cbm_type_named(arena, "std.pair")); + reg_method(reg, arena, map_qn, "erase", t_size_t); + reg_method(reg, arena, map_qn, "count", t_size_t); + reg_method(reg, arena, map_qn, "contains", t_bool); + reg_method(reg, arena, map_qn, "size", t_size_t); + reg_method(reg, arena, map_qn, "empty", t_bool); + reg_method(reg, arena, map_qn, "clear", t_void); + reg_method(reg, arena, map_qn, "begin", cbm_type_named(arena, "std.map.iterator")); + reg_method(reg, arena, map_qn, "end", cbm_type_named(arena, "std.map.iterator")); + + // std.unordered_map — same interface + const char* umap_qn = "std.unordered_map"; + reg_type(reg, umap_qn, "unordered_map"); + reg_method(reg, arena, umap_qn, "operator[]", t_V_ref); + reg_method(reg, arena, umap_qn, "at", t_V_ref); + reg_method(reg, arena, umap_qn, "find", cbm_type_named(arena, "std.unordered_map.iterator")); + reg_method(reg, arena, umap_qn, "insert", cbm_type_named(arena, "std.pair")); + reg_method(reg, arena, umap_qn, "emplace", cbm_type_named(arena, "std.pair")); + reg_method(reg, arena, umap_qn, "erase", t_size_t); + reg_method(reg, arena, umap_qn, "count", t_size_t); + reg_method(reg, arena, umap_qn, "contains", t_bool); + reg_method(reg, arena, umap_qn, "size", t_size_t); + reg_method(reg, arena, umap_qn, "empty", t_bool); + reg_method(reg, arena, umap_qn, "clear", t_void); + reg_method(reg, arena, umap_qn, "begin", cbm_type_named(arena, "std.unordered_map.iterator")); + reg_method(reg, arena, umap_qn, "end", cbm_type_named(arena, "std.unordered_map.iterator")); + + // ========================================================================= + // std.set / std.unordered_set + // ========================================================================= + const char* set_qn = "std.set"; + reg_type(reg, set_qn, "set"); + reg_method(reg, arena, set_qn, "insert", cbm_type_named(arena, "std.pair")); + reg_method(reg, arena, set_qn, "find", cbm_type_named(arena, "std.set.iterator")); + reg_method(reg, arena, set_qn, "erase", t_size_t); + reg_method(reg, arena, set_qn, "count", t_size_t); + reg_method(reg, arena, set_qn, "contains", t_bool); + reg_method(reg, arena, set_qn, "size", t_size_t); + reg_method(reg, arena, set_qn, "empty", t_bool); + reg_method(reg, arena, set_qn, "clear", t_void); + reg_method(reg, arena, set_qn, "begin", cbm_type_named(arena, "std.set.iterator")); + reg_method(reg, arena, set_qn, "end", cbm_type_named(arena, "std.set.iterator")); + + const char* uset_qn = "std.unordered_set"; + reg_type(reg, uset_qn, "unordered_set"); + reg_method(reg, arena, uset_qn, "insert", cbm_type_named(arena, "std.pair")); + reg_method(reg, arena, uset_qn, "find", cbm_type_named(arena, "std.unordered_set.iterator")); + reg_method(reg, arena, uset_qn, "erase", t_size_t); + reg_method(reg, arena, uset_qn, "count", t_size_t); + reg_method(reg, arena, uset_qn, "contains", t_bool); + reg_method(reg, arena, uset_qn, "size", t_size_t); + reg_method(reg, arena, uset_qn, "empty", t_bool); + reg_method(reg, arena, uset_qn, "clear", t_void); + + // ========================================================================= + // Smart pointers: std.unique_ptr, std.shared_ptr, std.weak_ptr + // ========================================================================= + const char* uptr_qn = "std.unique_ptr"; + reg_type(reg, uptr_qn, "unique_ptr"); + reg_method(reg, arena, uptr_qn, "get", t_T_ptr); + reg_method(reg, arena, uptr_qn, "reset", t_void); + reg_method(reg, arena, uptr_qn, "release", t_T_ptr); + reg_method(reg, arena, uptr_qn, "operator->", t_T_ptr); + reg_method(reg, arena, uptr_qn, "operator*", t_T_ref); + reg_method(reg, arena, uptr_qn, "operator bool", t_bool); + reg_method(reg, arena, uptr_qn, "swap", t_void); + + const char* sptr_qn = "std.shared_ptr"; + reg_type(reg, sptr_qn, "shared_ptr"); + reg_method(reg, arena, sptr_qn, "get", t_T_ptr); + reg_method(reg, arena, sptr_qn, "reset", t_void); + reg_method(reg, arena, sptr_qn, "use_count", t_long); + reg_method(reg, arena, sptr_qn, "unique", t_bool); + reg_method(reg, arena, sptr_qn, "operator->", t_T_ptr); + reg_method(reg, arena, sptr_qn, "operator*", t_T_ref); + reg_method(reg, arena, sptr_qn, "operator bool", t_bool); + reg_method(reg, arena, sptr_qn, "swap", t_void); + + const char* wptr_qn = "std.weak_ptr"; + reg_type(reg, wptr_qn, "weak_ptr"); + reg_method(reg, arena, wptr_qn, "lock", cbm_type_named(arena, sptr_qn)); + reg_method(reg, arena, wptr_qn, "use_count", t_long); + reg_method(reg, arena, wptr_qn, "expired", t_bool); + reg_method(reg, arena, wptr_qn, "reset", t_void); + + // std.make_unique / std.make_shared — free functions + // Return type depends on template arg, registered as returning unique_ptr/shared_ptr + reg_func(reg, arena, "std.make_unique", "make_unique", + cbm_type_named(arena, uptr_qn)); + reg_func(reg, arena, "std.make_shared", "make_shared", + cbm_type_named(arena, sptr_qn)); + + // ========================================================================= + // std.optional + // ========================================================================= + const char* opt_qn = "std.optional"; + reg_type(reg, opt_qn, "optional"); + reg_method(reg, arena, opt_qn, "value", t_T_ref); + reg_method(reg, arena, opt_qn, "has_value", t_bool); + reg_method(reg, arena, opt_qn, "value_or", t_T); + reg_method(reg, arena, opt_qn, "operator*", t_T_ref); + reg_method(reg, arena, opt_qn, "operator->", t_T_ptr); + reg_method(reg, arena, opt_qn, "operator bool", t_bool); + reg_method(reg, arena, opt_qn, "reset", t_void); + reg_method(reg, arena, opt_qn, "emplace", t_T_ref); + + // ========================================================================= + // std.pair + // ========================================================================= + const char* pair_qn = "std.pair"; + { + static const char* pair_field_names[] = {"first", "second", NULL}; + const CBMType* pair_field_types[] = { + cbm_type_type_param(arena, "T1"), + cbm_type_type_param(arena, "T2"), + NULL + }; + reg_type_with_fields(reg, arena, pair_qn, "pair", pair_field_names, pair_field_types); + } + + // ========================================================================= + // std.tuple<...> + // ========================================================================= + reg_type(reg, "std.tuple", "tuple"); + reg_func(reg, arena, "std.get", "get", cbm_type_unknown()); + + // ========================================================================= + // std.array + // ========================================================================= + const char* arr_qn = "std.array"; + reg_type(reg, arr_qn, "array"); + reg_method(reg, arena, arr_qn, "size", t_size_t); + reg_method(reg, arena, arr_qn, "data", t_T_ptr); + reg_method(reg, arena, arr_qn, "at", t_T_ref); + reg_method(reg, arena, arr_qn, "front", t_T_ref); + reg_method(reg, arena, arr_qn, "back", t_T_ref); + reg_method(reg, arena, arr_qn, "operator[]", t_T_ref); + reg_method(reg, arena, arr_qn, "begin", t_T_ptr); + reg_method(reg, arena, arr_qn, "end", t_T_ptr); + reg_method(reg, arena, arr_qn, "empty", t_bool); + reg_method(reg, arena, arr_qn, "fill", t_void); + + // ========================================================================= + // std.function + // ========================================================================= + reg_type(reg, "std.function", "function"); + reg_method(reg, arena, "std.function", "operator()", cbm_type_unknown()); + reg_method(reg, arena, "std.function", "operator bool", t_bool); + reg_method(reg, arena, "std.function", "target", t_void_ptr); + + // ========================================================================= + // I/O streams + // ========================================================================= + const char* os_qn = "std.ostream"; + reg_type(reg, os_qn, "ostream"); + const CBMType* t_ostream_ref = cbm_type_reference(arena, cbm_type_named(arena, os_qn)); + reg_method(reg, arena, os_qn, "operator<<", t_ostream_ref); + reg_method(reg, arena, os_qn, "write", t_ostream_ref); + reg_method(reg, arena, os_qn, "flush", t_ostream_ref); + reg_method(reg, arena, os_qn, "put", t_ostream_ref); + reg_method(reg, arena, os_qn, "good", t_bool); + reg_method(reg, arena, os_qn, "fail", t_bool); + reg_method(reg, arena, os_qn, "bad", t_bool); + reg_method(reg, arena, os_qn, "eof", t_bool); + + const char* is_qn = "std.istream"; + reg_type(reg, is_qn, "istream"); + const CBMType* t_istream_ref = cbm_type_reference(arena, cbm_type_named(arena, is_qn)); + reg_method(reg, arena, is_qn, "operator>>", t_istream_ref); + reg_method(reg, arena, is_qn, "read", t_istream_ref); + reg_method(reg, arena, is_qn, "getline", t_istream_ref); + reg_method(reg, arena, is_qn, "get", t_int); + reg_method(reg, arena, is_qn, "peek", t_int); + reg_method(reg, arena, is_qn, "good", t_bool); + reg_method(reg, arena, is_qn, "fail", t_bool); + + // Global stream objects + reg_func(reg, arena, "std.cout", "cout", cbm_type_named(arena, os_qn)); + reg_func(reg, arena, "std.cin", "cin", cbm_type_named(arena, is_qn)); + reg_func(reg, arena, "std.cerr", "cerr", cbm_type_named(arena, os_qn)); + reg_func(reg, arena, "std.clog", "clog", cbm_type_named(arena, os_qn)); + + // std.endl, std.flush — manipulators (treat as returning ostream ref) + reg_func(reg, arena, "std.endl", "endl", t_ostream_ref); + reg_func(reg, arena, "std.flush", "flush", t_ostream_ref); + + // ========================================================================= + // std.filesystem.path + // ========================================================================= + const char* fspath_qn = "std.filesystem.path"; + reg_type(reg, fspath_qn, "path"); + const CBMType* t_path = cbm_type_named(arena, fspath_qn); + reg_method(reg, arena, fspath_qn, "string", t_string); + reg_method(reg, arena, fspath_qn, "c_str", t_char_ptr); + reg_method(reg, arena, fspath_qn, "extension", t_path); + reg_method(reg, arena, fspath_qn, "filename", t_path); + reg_method(reg, arena, fspath_qn, "parent_path", t_path); + reg_method(reg, arena, fspath_qn, "stem", t_path); + reg_method(reg, arena, fspath_qn, "root_path", t_path); + reg_method(reg, arena, fspath_qn, "is_absolute", t_bool); + reg_method(reg, arena, fspath_qn, "is_relative", t_bool); + reg_method(reg, arena, fspath_qn, "empty", t_bool); + reg_method(reg, arena, fspath_qn, "exists", t_bool); + reg_method(reg, arena, fspath_qn, "operator/", t_path); + reg_method(reg, arena, fspath_qn, "operator/=", cbm_type_reference(arena, t_path)); + + // ========================================================================= + // std.thread + // ========================================================================= + const char* thread_qn = "std.thread"; + reg_type(reg, thread_qn, "thread"); + reg_method(reg, arena, thread_qn, "join", t_void); + reg_method(reg, arena, thread_qn, "detach", t_void); + reg_method(reg, arena, thread_qn, "joinable", t_bool); + reg_method(reg, arena, thread_qn, "get_id", cbm_type_named(arena, "std.thread.id")); + + // ========================================================================= + // std.mutex / std.lock_guard / std.unique_lock + // ========================================================================= + const char* mutex_qn = "std.mutex"; + reg_type(reg, mutex_qn, "mutex"); + reg_method(reg, arena, mutex_qn, "lock", t_void); + reg_method(reg, arena, mutex_qn, "unlock", t_void); + reg_method(reg, arena, mutex_qn, "try_lock", t_bool); + + reg_type(reg, "std.lock_guard", "lock_guard"); + reg_type(reg, "std.unique_lock", "unique_lock"); + reg_method(reg, arena, "std.unique_lock", "lock", t_void); + reg_method(reg, arena, "std.unique_lock", "unlock", t_void); + reg_method(reg, arena, "std.unique_lock", "try_lock", t_bool); + reg_method(reg, arena, "std.unique_lock", "owns_lock", t_bool); + + // ========================================================================= + // std.chrono basics + // ========================================================================= + reg_type(reg, "std.chrono.seconds", "seconds"); + reg_type(reg, "std.chrono.milliseconds", "milliseconds"); + reg_type(reg, "std.chrono.microseconds", "microseconds"); + reg_type(reg, "std.chrono.nanoseconds", "nanoseconds"); + reg_type(reg, "std.chrono.system_clock", "system_clock"); + reg_type(reg, "std.chrono.steady_clock", "steady_clock"); + reg_func(reg, arena, "std.chrono.system_clock.now", "now", + cbm_type_named(arena, "std.chrono.time_point")); + reg_func(reg, arena, "std.chrono.steady_clock.now", "now", + cbm_type_named(arena, "std.chrono.time_point")); + + // ========================================================================= + // std.algorithm free functions + // ========================================================================= + reg_func(reg, arena, "std.sort", "sort", t_void); + reg_func(reg, arena, "std.find", "find", cbm_type_unknown()); + reg_func(reg, arena, "std.find_if", "find_if", cbm_type_unknown()); + reg_func(reg, arena, "std.for_each", "for_each", cbm_type_unknown()); + reg_func(reg, arena, "std.transform", "transform", cbm_type_unknown()); + reg_func(reg, arena, "std.copy", "copy", cbm_type_unknown()); + reg_func(reg, arena, "std.move", "move", cbm_type_unknown()); + reg_func(reg, arena, "std.swap", "swap", t_void); + reg_func(reg, arena, "std.min", "min", cbm_type_unknown()); + reg_func(reg, arena, "std.max", "max", cbm_type_unknown()); + reg_func(reg, arena, "std.accumulate", "accumulate", cbm_type_unknown()); + reg_func(reg, arena, "std.count", "count", t_size_t); + reg_func(reg, arena, "std.count_if", "count_if", t_size_t); + reg_func(reg, arena, "std.remove", "remove", cbm_type_unknown()); + reg_func(reg, arena, "std.remove_if", "remove_if", cbm_type_unknown()); + reg_func(reg, arena, "std.reverse", "reverse", t_void); + reg_func(reg, arena, "std.unique", "unique", cbm_type_unknown()); + reg_func(reg, arena, "std.lower_bound", "lower_bound", cbm_type_unknown()); + reg_func(reg, arena, "std.upper_bound", "upper_bound", cbm_type_unknown()); + reg_func(reg, arena, "std.binary_search", "binary_search", t_bool); + + // std.to_string + reg_func(reg, arena, "std.to_string", "to_string", t_string); + reg_func(reg, arena, "std.stoi", "stoi", t_int); + reg_func(reg, arena, "std.stol", "stol", t_long); + reg_func(reg, arena, "std.stof", "stof", cbm_type_builtin(arena, "float")); + reg_func(reg, arena, "std.stod", "stod", cbm_type_builtin(arena, "double")); + + // ========================================================================= + // ========================================================================= + // std.variant + // ========================================================================= + reg_type(reg, "std.variant", "variant"); + reg_method(reg, arena, "std.variant", "index", t_size_t); + reg_method(reg, arena, "std.variant", "valueless_by_exception", t_bool); + reg_func(reg, arena, "std.holds_alternative", "holds_alternative", t_bool); + reg_func(reg, arena, "std.get_if", "get_if", t_T_ptr); + + // ========================================================================= + // std.any + // ========================================================================= + reg_type(reg, "std.any", "any"); + reg_method(reg, arena, "std.any", "has_value", t_bool); + reg_method(reg, arena, "std.any", "type", cbm_type_unknown()); // type_info + reg_method(reg, arena, "std.any", "reset", t_void); + reg_func(reg, arena, "std.any_cast", "any_cast", t_T); + reg_func(reg, arena, "std.make_any", "make_any", cbm_type_named(arena, "std.any")); + + // ========================================================================= + // std.regex / std.smatch + // ========================================================================= + reg_type(reg, "std.regex", "regex"); + reg_type(reg, "std.smatch", "smatch"); + reg_method(reg, arena, "std.smatch", "size", t_size_t); + reg_method(reg, arena, "std.smatch", "empty", t_bool); + reg_method(reg, arena, "std.smatch", "str", t_string); + reg_method(reg, arena, "std.smatch", "prefix", cbm_type_unknown()); + reg_method(reg, arena, "std.smatch", "suffix", cbm_type_unknown()); + reg_func(reg, arena, "std.regex_search", "regex_search", t_bool); + reg_func(reg, arena, "std.regex_match", "regex_match", t_bool); + reg_func(reg, arena, "std.regex_replace", "regex_replace", t_string); + + // ========================================================================= + // std.pair + // ========================================================================= + reg_type(reg, "std.pair", "pair"); + reg_func(reg, arena, "std.make_pair", "make_pair", + cbm_type_named(arena, "std.pair")); + + // ========================================================================= + // algorithms + // ========================================================================= + reg_func(reg, arena, "std.accumulate", "accumulate", t_T); + reg_func(reg, arena, "std.inner_product", "inner_product", t_T); + reg_func(reg, arena, "std.iota", "iota", t_void); + reg_func(reg, arena, "std.partial_sum", "partial_sum", cbm_type_unknown()); + reg_func(reg, arena, "std.adjacent_difference", "adjacent_difference", cbm_type_unknown()); + reg_func(reg, arena, "std.reduce", "reduce", t_T); + reg_func(reg, arena, "std.transform_reduce", "transform_reduce", t_T); + + // ========================================================================= + // functions + // ========================================================================= + reg_func(reg, arena, "std.advance", "advance", t_void); + reg_func(reg, arena, "std.distance", "distance", cbm_type_builtin(arena, "ptrdiff_t")); + reg_func(reg, arena, "std.next", "next", cbm_type_unknown()); // iterator + reg_func(reg, arena, "std.prev", "prev", cbm_type_unknown()); + reg_func(reg, arena, "std.begin", "begin", cbm_type_unknown()); + reg_func(reg, arena, "std.end", "end", cbm_type_unknown()); + reg_func(reg, arena, "std.rbegin", "rbegin", cbm_type_unknown()); + reg_func(reg, arena, "std.rend", "rend", cbm_type_unknown()); + + // ========================================================================= + // — additional entries with better return types + // ========================================================================= + reg_func(reg, arena, "std.for_each", "for_each", cbm_type_unknown()); + reg_func(reg, arena, "std.all_of", "all_of", t_bool); + reg_func(reg, arena, "std.any_of", "any_of", t_bool); + reg_func(reg, arena, "std.none_of", "none_of", t_bool); + reg_func(reg, arena, "std.min", "min", t_T_ref); + reg_func(reg, arena, "std.max", "max", t_T_ref); + reg_func(reg, arena, "std.min_element", "min_element", cbm_type_unknown()); + reg_func(reg, arena, "std.max_element", "max_element", cbm_type_unknown()); + reg_func(reg, arena, "std.minmax", "minmax", + cbm_type_named(arena, "std.pair")); + reg_func(reg, arena, "std.clamp", "clamp", t_T_ref); + reg_func(reg, arena, "std.swap", "swap", t_void); + reg_func(reg, arena, "std.fill", "fill", t_void); + reg_func(reg, arena, "std.replace", "replace", t_void); + reg_func(reg, arena, "std.replace_if", "replace_if", t_void); + reg_func(reg, arena, "std.equal", "equal", t_bool); + reg_func(reg, arena, "std.mismatch", "mismatch", + cbm_type_named(arena, "std.pair")); + reg_func(reg, arena, "std.lexicographical_compare", "lexicographical_compare", t_bool); + reg_func(reg, arena, "std.partition", "partition", cbm_type_unknown()); + reg_func(reg, arena, "std.stable_partition", "stable_partition", cbm_type_unknown()); + reg_func(reg, arena, "std.nth_element", "nth_element", t_void); + reg_func(reg, arena, "std.partial_sort", "partial_sort", t_void); + reg_func(reg, arena, "std.stable_sort", "stable_sort", t_void); + reg_func(reg, arena, "std.is_sorted", "is_sorted", t_bool); + reg_func(reg, arena, "std.merge", "merge", cbm_type_unknown()); + reg_func(reg, arena, "std.includes", "includes", t_bool); + reg_func(reg, arena, "std.set_union", "set_union", cbm_type_unknown()); + reg_func(reg, arena, "std.set_intersection", "set_intersection", cbm_type_unknown()); + reg_func(reg, arena, "std.set_difference", "set_difference", cbm_type_unknown()); + reg_func(reg, arena, "std.generate", "generate", t_void); + reg_func(reg, arena, "std.generate_n", "generate_n", cbm_type_unknown()); + + // ========================================================================= + // — additional + // ========================================================================= + reg_func(reg, arena, "std.addressof", "addressof", t_T_ptr); + reg_func(reg, arena, "std.allocate_shared", "allocate_shared", + cbm_type_named(arena, sptr_qn)); + + // ========================================================================= + // + // ========================================================================= + reg_func(reg, arena, "std.move", "move", t_T); + reg_func(reg, arena, "std.forward", "forward", t_T); + reg_func(reg, arena, "std.exchange", "exchange", t_T); + reg_func(reg, arena, "std.declval", "declval", t_T); + reg_func(reg, arena, "std.as_const", "as_const", t_T_ref); + + // ========================================================================= + // — common query functions + // ========================================================================= + reg_func(reg, arena, "std.is_same_v", "is_same_v", t_bool); + reg_func(reg, arena, "std.is_base_of_v", "is_base_of_v", t_bool); + + // ========================================================================= + // std.deque + // ========================================================================= + const char* deq_qn = "std.deque"; + reg_type(reg, deq_qn, "deque"); + reg_method(reg, arena, deq_qn, "push_back", t_void); + reg_method(reg, arena, deq_qn, "push_front", t_void); + reg_method(reg, arena, deq_qn, "pop_back", t_void); + reg_method(reg, arena, deq_qn, "pop_front", t_void); + reg_method(reg, arena, deq_qn, "front", t_T_ref); + reg_method(reg, arena, deq_qn, "back", t_T_ref); + reg_method(reg, arena, deq_qn, "size", t_size_t); + reg_method(reg, arena, deq_qn, "empty", t_bool); + reg_method(reg, arena, deq_qn, "clear", t_void); + reg_method(reg, arena, deq_qn, "at", t_T_ref); + + // ========================================================================= + // std.list + // ========================================================================= + const char* list_qn = "std.list"; + reg_type(reg, list_qn, "list"); + reg_method(reg, arena, list_qn, "push_back", t_void); + reg_method(reg, arena, list_qn, "push_front", t_void); + reg_method(reg, arena, list_qn, "pop_back", t_void); + reg_method(reg, arena, list_qn, "pop_front", t_void); + reg_method(reg, arena, list_qn, "front", t_T_ref); + reg_method(reg, arena, list_qn, "back", t_T_ref); + reg_method(reg, arena, list_qn, "size", t_size_t); + reg_method(reg, arena, list_qn, "empty", t_bool); + reg_method(reg, arena, list_qn, "clear", t_void); + reg_method(reg, arena, list_qn, "sort", t_void); + reg_method(reg, arena, list_qn, "reverse", t_void); + reg_method(reg, arena, list_qn, "merge", t_void); + reg_method(reg, arena, list_qn, "unique", t_void); + + // ========================================================================= + // std.stack / std.queue / std.priority_queue + // ========================================================================= + const char* stack_qn = "std.stack"; + reg_type(reg, stack_qn, "stack"); + reg_method(reg, arena, stack_qn, "push", t_void); + reg_method(reg, arena, stack_qn, "pop", t_void); + reg_method(reg, arena, stack_qn, "top", t_T_ref); + reg_method(reg, arena, stack_qn, "size", t_size_t); + reg_method(reg, arena, stack_qn, "empty", t_bool); + + const char* queue_qn = "std.queue"; + reg_type(reg, queue_qn, "queue"); + reg_method(reg, arena, queue_qn, "push", t_void); + reg_method(reg, arena, queue_qn, "pop", t_void); + reg_method(reg, arena, queue_qn, "front", t_T_ref); + reg_method(reg, arena, queue_qn, "back", t_T_ref); + reg_method(reg, arena, queue_qn, "size", t_size_t); + reg_method(reg, arena, queue_qn, "empty", t_bool); + + const char* pq_qn = "std.priority_queue"; + reg_type(reg, pq_qn, "priority_queue"); + reg_method(reg, arena, pq_qn, "push", t_void); + reg_method(reg, arena, pq_qn, "pop", t_void); + reg_method(reg, arena, pq_qn, "top", t_T_ref); + reg_method(reg, arena, pq_qn, "size", t_size_t); + reg_method(reg, arena, pq_qn, "empty", t_bool); + + // ========================================================================= + // std.bitset + // ========================================================================= + reg_type(reg, "std.bitset", "bitset"); + reg_method(reg, arena, "std.bitset", "set", cbm_type_named(arena, "std.bitset")); + reg_method(reg, arena, "std.bitset", "reset", cbm_type_named(arena, "std.bitset")); + reg_method(reg, arena, "std.bitset", "flip", cbm_type_named(arena, "std.bitset")); + reg_method(reg, arena, "std.bitset", "test", t_bool); + reg_method(reg, arena, "std.bitset", "count", t_size_t); + reg_method(reg, arena, "std.bitset", "size", t_size_t); + reg_method(reg, arena, "std.bitset", "any", t_bool); + reg_method(reg, arena, "std.bitset", "none", t_bool); + reg_method(reg, arena, "std.bitset", "all", t_bool); + reg_method(reg, arena, "std.bitset", "to_string", t_string); + reg_method(reg, arena, "std.bitset", "to_ulong", cbm_type_builtin(arena, "unsigned long")); + + // ========================================================================= + // std.stringstream / std.ostringstream / std.istringstream + // ========================================================================= + const char* ss_qn = "std.stringstream"; + reg_type(reg, ss_qn, "stringstream"); + reg_method(reg, arena, ss_qn, "str", t_string); + reg_method(reg, arena, ss_qn, "clear", t_void); + + const char* oss_qn = "std.ostringstream"; + reg_type(reg, oss_qn, "ostringstream"); + reg_method(reg, arena, oss_qn, "str", t_string); + reg_method(reg, arena, oss_qn, "clear", t_void); + + const char* iss_qn = "std.istringstream"; + reg_type(reg, iss_qn, "istringstream"); + reg_method(reg, arena, iss_qn, "str", t_string); + reg_method(reg, arena, iss_qn, "clear", t_void); + + // ========================================================================= + // std.filesystem — additional + // ========================================================================= + reg_func(reg, arena, "std.filesystem.exists", "exists", t_bool); + reg_func(reg, arena, "std.filesystem.is_directory", "is_directory", t_bool); + reg_func(reg, arena, "std.filesystem.is_regular_file", "is_regular_file", t_bool); + reg_func(reg, arena, "std.filesystem.file_size", "file_size", cbm_type_builtin(arena, "uintmax_t")); + reg_func(reg, arena, "std.filesystem.create_directory", "create_directory", t_bool); + reg_func(reg, arena, "std.filesystem.create_directories", "create_directories", t_bool); + reg_func(reg, arena, "std.filesystem.remove", "remove", t_bool); + reg_func(reg, arena, "std.filesystem.remove_all", "remove_all", cbm_type_builtin(arena, "uintmax_t")); + reg_func(reg, arena, "std.filesystem.copy", "copy", t_void); + reg_func(reg, arena, "std.filesystem.rename", "rename", t_void); + reg_func(reg, arena, "std.filesystem.current_path", "current_path", + cbm_type_named(arena, fspath_qn)); + reg_func(reg, arena, "std.filesystem.absolute", "absolute", + cbm_type_named(arena, fspath_qn)); + reg_func(reg, arena, "std.filesystem.canonical", "canonical", + cbm_type_named(arena, fspath_qn)); + reg_func(reg, arena, "std.filesystem.relative", "relative", + cbm_type_named(arena, fspath_qn)); + reg_func(reg, arena, "std.filesystem.temp_directory_path", "temp_directory_path", + cbm_type_named(arena, fspath_qn)); + + const char* direntry_qn = "std.filesystem.directory_entry"; + reg_type(reg, direntry_qn, "directory_entry"); + reg_method(reg, arena, direntry_qn, "path", cbm_type_named(arena, fspath_qn)); + reg_method(reg, arena, direntry_qn, "exists", t_bool); + reg_method(reg, arena, direntry_qn, "is_directory", t_bool); + reg_method(reg, arena, direntry_qn, "is_regular_file", t_bool); + reg_method(reg, arena, direntry_qn, "file_size", cbm_type_builtin(arena, "uintmax_t")); + + // ========================================================================= + // std.chrono — additional + // ========================================================================= + reg_type(reg, "std.chrono.time_point", "time_point"); + reg_method(reg, arena, "std.chrono.time_point", "time_since_epoch", cbm_type_unknown()); + reg_method(reg, arena, "std.chrono.system_clock", "now", + cbm_type_named(arena, "std.chrono.time_point")); + reg_method(reg, arena, "std.chrono.steady_clock", "now", + cbm_type_named(arena, "std.chrono.time_point")); + reg_func(reg, arena, "std.chrono.duration_cast", "duration_cast", cbm_type_unknown()); + + // ========================================================================= + // Boost smart pointers (common in older codebases) + // ========================================================================= + const char* bsptr_qn = "boost.shared_ptr"; + reg_type(reg, bsptr_qn, "shared_ptr"); + reg_method(reg, arena, bsptr_qn, "get", t_T_ptr); + reg_method(reg, arena, bsptr_qn, "reset", t_void); + reg_method(reg, arena, bsptr_qn, "use_count", t_long); + reg_method(reg, arena, bsptr_qn, "operator->", t_T_ptr); + reg_method(reg, arena, bsptr_qn, "operator*", t_T_ref); + + const char* bscoped_qn = "boost.scoped_ptr"; + reg_type(reg, bscoped_qn, "scoped_ptr"); + reg_method(reg, arena, bscoped_qn, "get", t_T_ptr); + reg_method(reg, arena, bscoped_qn, "reset", t_void); + reg_method(reg, arena, bscoped_qn, "operator->", t_T_ptr); + reg_method(reg, arena, bscoped_qn, "operator*", t_T_ref); + + // ========================================================================= + // Boost optional / filesystem + // ========================================================================= + const char* bopt_qn = "boost.optional"; + reg_type(reg, bopt_qn, "optional"); + reg_method(reg, arena, bopt_qn, "value", t_T_ref); + reg_method(reg, arena, bopt_qn, "get", t_T_ref); + reg_method(reg, arena, bopt_qn, "value_or", t_T); + reg_method(reg, arena, bopt_qn, "is_initialized", t_bool); + reg_method(reg, arena, bopt_qn, "operator*", t_T_ref); + reg_method(reg, arena, bopt_qn, "operator->", t_T_ptr); + + const char* bfspath_qn = "boost.filesystem.path"; + reg_type(reg, bfspath_qn, "path"); + reg_method(reg, arena, bfspath_qn, "string", t_string); + reg_method(reg, arena, bfspath_qn, "parent_path", cbm_type_named(arena, bfspath_qn)); + reg_method(reg, arena, bfspath_qn, "filename", cbm_type_named(arena, bfspath_qn)); + reg_method(reg, arena, bfspath_qn, "extension", cbm_type_named(arena, bfspath_qn)); + reg_method(reg, arena, bfspath_qn, "stem", cbm_type_named(arena, bfspath_qn)); + reg_method(reg, arena, bfspath_qn, "empty", t_bool); + reg_method(reg, arena, bfspath_qn, "exists", t_bool); + + // ========================================================================= + // Protobuf (google.protobuf.Message) + // ========================================================================= + const char* pb_msg_qn = "google.protobuf.Message"; + reg_type(reg, pb_msg_qn, "Message"); + reg_method(reg, arena, pb_msg_qn, "SerializeToString", t_bool); + reg_method(reg, arena, pb_msg_qn, "SerializeAsString", t_string); + reg_method(reg, arena, pb_msg_qn, "ParseFromString", t_bool); + reg_method(reg, arena, pb_msg_qn, "ParseFromArray", t_bool); + reg_method(reg, arena, pb_msg_qn, "DebugString", t_string); + reg_method(reg, arena, pb_msg_qn, "ShortDebugString", t_string); + reg_method(reg, arena, pb_msg_qn, "ByteSizeLong", t_size_t); + reg_method(reg, arena, pb_msg_qn, "IsInitialized", t_bool); + reg_method(reg, arena, pb_msg_qn, "Clear", t_void); + reg_method(reg, arena, pb_msg_qn, "CopyFrom", t_void); + reg_method(reg, arena, pb_msg_qn, "MergeFrom", t_void); + reg_method(reg, arena, pb_msg_qn, "GetTypeName", t_string); + // MessageLite is base of Message + reg_type(reg, "google.protobuf.MessageLite", "MessageLite"); + + // ========================================================================= + // Abseil (absl.) + // ========================================================================= + const char* absv_qn = "absl.string_view"; + reg_type(reg, absv_qn, "string_view"); + reg_method(reg, arena, absv_qn, "data", cbm_type_pointer(arena, cbm_type_builtin(arena, "char"))); + reg_method(reg, arena, absv_qn, "size", t_size_t); + reg_method(reg, arena, absv_qn, "length", t_size_t); + reg_method(reg, arena, absv_qn, "empty", t_bool); + reg_method(reg, arena, absv_qn, "substr", cbm_type_named(arena, absv_qn)); + + const char* abstat_qn = "absl.Status"; + reg_type(reg, abstat_qn, "Status"); + reg_method(reg, arena, abstat_qn, "ok", t_bool); + reg_method(reg, arena, abstat_qn, "code", cbm_type_unknown()); + reg_method(reg, arena, abstat_qn, "message", cbm_type_named(arena, absv_qn)); + reg_method(reg, arena, abstat_qn, "ToString", t_string); + reg_func(reg, arena, "absl.OkStatus", "OkStatus", cbm_type_named(arena, abstat_qn)); + + const char* absor_qn = "absl.StatusOr"; + reg_type(reg, absor_qn, "StatusOr"); + reg_method(reg, arena, absor_qn, "ok", t_bool); + reg_method(reg, arena, absor_qn, "status", cbm_type_named(arena, abstat_qn)); + reg_method(reg, arena, absor_qn, "value", t_T_ref); + reg_method(reg, arena, absor_qn, "operator*", t_T_ref); + reg_method(reg, arena, absor_qn, "operator->", t_T_ptr); + + const char* abfhm_qn = "absl.flat_hash_map"; + reg_type(reg, abfhm_qn, "flat_hash_map"); + reg_method(reg, arena, abfhm_qn, "find", cbm_type_unknown()); + reg_method(reg, arena, abfhm_qn, "contains", t_bool); + reg_method(reg, arena, abfhm_qn, "size", t_size_t); + reg_method(reg, arena, abfhm_qn, "empty", t_bool); + reg_method(reg, arena, abfhm_qn, "clear", t_void); + reg_method(reg, arena, abfhm_qn, "insert", cbm_type_unknown()); + reg_method(reg, arena, abfhm_qn, "erase", t_size_t); + reg_method(reg, arena, abfhm_qn, "count", t_size_t); + + const char* abfhs_qn = "absl.flat_hash_set"; + reg_type(reg, abfhs_qn, "flat_hash_set"); + reg_method(reg, arena, abfhs_qn, "contains", t_bool); + reg_method(reg, arena, abfhs_qn, "size", t_size_t); + reg_method(reg, arena, abfhs_qn, "empty", t_bool); + reg_method(reg, arena, abfhs_qn, "clear", t_void); + reg_method(reg, arena, abfhs_qn, "insert", cbm_type_unknown()); + reg_method(reg, arena, abfhs_qn, "erase", t_size_t); + reg_method(reg, arena, abfhs_qn, "count", t_size_t); + + const char* abspan_qn = "absl.Span"; + reg_type(reg, abspan_qn, "Span"); + reg_method(reg, arena, abspan_qn, "data", t_T_ptr); + reg_method(reg, arena, abspan_qn, "size", t_size_t); + reg_method(reg, arena, abspan_qn, "empty", t_bool); + reg_method(reg, arena, abspan_qn, "front", t_T_ref); + reg_method(reg, arena, abspan_qn, "back", t_T_ref); + reg_method(reg, arena, abspan_qn, "at", t_T_ref); + reg_method(reg, arena, abspan_qn, "subspan", cbm_type_named(arena, abspan_qn)); + + // absl string functions + reg_func(reg, arena, "absl.StrCat", "StrCat", t_string); + reg_func(reg, arena, "absl.StrAppend", "StrAppend", t_void); + reg_func(reg, arena, "absl.StrJoin", "StrJoin", t_string); + reg_func(reg, arena, "absl.StrSplit", "StrSplit", + cbm_type_named(arena, "std.vector")); + reg_func(reg, arena, "absl.StrFormat", "StrFormat", t_string); + reg_func(reg, arena, "absl.Substitute", "Substitute", t_string); + + // ========================================================================= + // gRPC + // ========================================================================= + const char* grpc_status_qn = "grpc.Status"; + reg_type(reg, grpc_status_qn, "Status"); + reg_method(reg, arena, grpc_status_qn, "ok", t_bool); + reg_method(reg, arena, grpc_status_qn, "error_code", cbm_type_unknown()); + reg_method(reg, arena, grpc_status_qn, "error_message", t_string); + reg_func(reg, arena, "grpc.Status.OK", "OK", cbm_type_named(arena, grpc_status_qn)); + + const char* grpc_sb_qn = "grpc.ServerBuilder"; + reg_type(reg, grpc_sb_qn, "ServerBuilder"); + reg_method(reg, arena, grpc_sb_qn, "AddListeningPort", cbm_type_named(arena, grpc_sb_qn)); + reg_method(reg, arena, grpc_sb_qn, "RegisterService", cbm_type_named(arena, grpc_sb_qn)); + reg_method(reg, arena, grpc_sb_qn, "BuildAndStart", cbm_type_unknown()); + + const char* grpc_chan_qn = "grpc.Channel"; + reg_type(reg, grpc_chan_qn, "Channel"); + reg_func(reg, arena, "grpc.CreateChannel", "CreateChannel", + cbm_type_named(arena, grpc_chan_qn)); + + const char* grpc_ctx_qn = "grpc.ClientContext"; + reg_type(reg, grpc_ctx_qn, "ClientContext"); + reg_method(reg, arena, grpc_ctx_qn, "set_deadline", t_void); + reg_method(reg, arena, grpc_ctx_qn, "AddMetadata", t_void); + + // ========================================================================= + // spdlog + // ========================================================================= + const char* spdlog_qn = "spdlog.logger"; + reg_type(reg, spdlog_qn, "logger"); + reg_method(reg, arena, spdlog_qn, "info", t_void); + reg_method(reg, arena, spdlog_qn, "warn", t_void); + reg_method(reg, arena, spdlog_qn, "error", t_void); + reg_method(reg, arena, spdlog_qn, "debug", t_void); + reg_method(reg, arena, spdlog_qn, "trace", t_void); + reg_method(reg, arena, spdlog_qn, "critical", t_void); + reg_method(reg, arena, spdlog_qn, "set_level", t_void); + reg_method(reg, arena, spdlog_qn, "flush", t_void); + reg_func(reg, arena, "spdlog.info", "info", t_void); + reg_func(reg, arena, "spdlog.warn", "warn", t_void); + reg_func(reg, arena, "spdlog.error", "error", t_void); + reg_func(reg, arena, "spdlog.debug", "debug", t_void); + reg_func(reg, arena, "spdlog.trace", "trace", t_void); + reg_func(reg, arena, "spdlog.critical", "critical", t_void); + reg_func(reg, arena, "spdlog.set_level", "set_level", t_void); + reg_func(reg, arena, "spdlog.get", "get", + cbm_type_named(arena, spdlog_qn)); + + // ========================================================================= + // Qt basics + // ========================================================================= + const char* qobj_qn = "QObject"; + reg_type(reg, qobj_qn, "QObject"); + reg_method(reg, arena, qobj_qn, "parent", cbm_type_pointer(arena, cbm_type_named(arena, qobj_qn))); + reg_method(reg, arena, qobj_qn, "children", cbm_type_unknown()); + reg_method(reg, arena, qobj_qn, "objectName", cbm_type_named(arena, "QString")); + reg_method(reg, arena, qobj_qn, "setObjectName", t_void); + reg_method(reg, arena, qobj_qn, "deleteLater", t_void); + + const char* qstr_qn = "QString"; + reg_type(reg, qstr_qn, "QString"); + reg_method(reg, arena, qstr_qn, "toStdString", t_string); + reg_method(reg, arena, qstr_qn, "toUtf8", cbm_type_unknown()); + reg_method(reg, arena, qstr_qn, "toLatin1", cbm_type_unknown()); + reg_method(reg, arena, qstr_qn, "size", t_int); + reg_method(reg, arena, qstr_qn, "length", t_int); + reg_method(reg, arena, qstr_qn, "isEmpty", t_bool); + reg_method(reg, arena, qstr_qn, "contains", t_bool); + reg_method(reg, arena, qstr_qn, "startsWith", t_bool); + reg_method(reg, arena, qstr_qn, "endsWith", t_bool); + reg_method(reg, arena, qstr_qn, "trimmed", cbm_type_named(arena, qstr_qn)); + reg_method(reg, arena, qstr_qn, "simplified", cbm_type_named(arena, qstr_qn)); + reg_method(reg, arena, qstr_qn, "toLower", cbm_type_named(arena, qstr_qn)); + reg_method(reg, arena, qstr_qn, "toUpper", cbm_type_named(arena, qstr_qn)); + reg_method(reg, arena, qstr_qn, "arg", cbm_type_named(arena, qstr_qn)); + reg_method(reg, arena, qstr_qn, "split", cbm_type_unknown()); + reg_method(reg, arena, qstr_qn, "replace", cbm_type_named(arena, qstr_qn)); + reg_method(reg, arena, qstr_qn, "mid", cbm_type_named(arena, qstr_qn)); + reg_method(reg, arena, qstr_qn, "left", cbm_type_named(arena, qstr_qn)); + reg_method(reg, arena, qstr_qn, "right", cbm_type_named(arena, qstr_qn)); + reg_method(reg, arena, qstr_qn, "toInt", t_int); + reg_method(reg, arena, qstr_qn, "toDouble", cbm_type_builtin(arena, "double")); + reg_func(reg, arena, "QString.number", "number", cbm_type_named(arena, qstr_qn)); + reg_func(reg, arena, "QString.fromStdString", "fromStdString", cbm_type_named(arena, qstr_qn)); + reg_func(reg, arena, "QString.fromUtf8", "fromUtf8", cbm_type_named(arena, qstr_qn)); + + const char* qwidget_qn = "QWidget"; + reg_type(reg, qwidget_qn, "QWidget"); + reg_method(reg, arena, qwidget_qn, "show", t_void); + reg_method(reg, arena, qwidget_qn, "hide", t_void); + reg_method(reg, arena, qwidget_qn, "close", t_bool); + reg_method(reg, arena, qwidget_qn, "setVisible", t_void); + reg_method(reg, arena, qwidget_qn, "isVisible", t_bool); + reg_method(reg, arena, qwidget_qn, "resize", t_void); + reg_method(reg, arena, qwidget_qn, "setWindowTitle", t_void); + reg_method(reg, arena, qwidget_qn, "update", t_void); + reg_method(reg, arena, qwidget_qn, "repaint", t_void); + reg_method(reg, arena, qwidget_qn, "setLayout", t_void); + +} diff --git a/internal/cbm/lsp/type_registry.c b/internal/cbm/lsp/type_registry.c index 8fd8cad3a..514da7936 100644 --- a/internal/cbm/lsp/type_registry.c +++ b/internal/cbm/lsp/type_registry.c @@ -76,6 +76,35 @@ const CBMRegisteredFunc* cbm_registry_lookup_func(const CBMTypeRegistry* reg, return NULL; } +const CBMRegisteredType* cbm_registry_resolve_alias(const CBMTypeRegistry* reg, const char* type_qn) { + if (!reg || !type_qn) return NULL; + const CBMRegisteredType* rt = cbm_registry_lookup_type(reg, type_qn); + for (int i = 0; i < 16 && rt && rt->alias_of; i++) { + const CBMRegisteredType* next = cbm_registry_lookup_type(reg, rt->alias_of); + if (!next) return rt; + rt = next; + } + return rt; +} + +const CBMRegisteredFunc* cbm_registry_lookup_method_aliased(const CBMTypeRegistry* reg, + const char* receiver_qn, const char* method_name) { + if (!reg || !receiver_qn || !method_name) return NULL; + + // Direct lookup first + const CBMRegisteredFunc* f = cbm_registry_lookup_method(reg, receiver_qn, method_name); + if (f) return f; + + // Follow alias chain + const CBMRegisteredType* rt = cbm_registry_lookup_type(reg, receiver_qn); + for (int i = 0; i < 16 && rt && rt->alias_of; i++) { + f = cbm_registry_lookup_method(reg, rt->alias_of, method_name); + if (f) return f; + rt = cbm_registry_lookup_type(reg, rt->alias_of); + } + return NULL; +} + const CBMRegisteredFunc* cbm_registry_lookup_symbol(const CBMTypeRegistry* reg, const char* package_qn, const char* name) { if (!reg || !package_qn || !name) return NULL; @@ -95,3 +124,180 @@ const CBMRegisteredFunc* cbm_registry_lookup_symbol(const CBMTypeRegistry* reg, return cbm_registry_lookup_func(reg, buf); } + +// Count parameters in a FUNC signature. +static int count_func_params(const CBMRegisteredFunc* f) { + if (!f || !f->signature || f->signature->kind != CBM_TYPE_FUNC) return -1; + if (!f->signature->data.func.param_types) return 0; + int count = 0; + while (f->signature->data.func.param_types[count]) count++; + return count; +} + +const CBMRegisteredFunc* cbm_registry_lookup_method_by_args(const CBMTypeRegistry* reg, + const char* receiver_qn, const char* method_name, int arg_count) { + if (!reg || !receiver_qn || !method_name) return NULL; + + const CBMRegisteredFunc* first_match = NULL; + for (int i = 0; i < reg->func_count; i++) { + const CBMRegisteredFunc* f = ®->funcs[i]; + if (f->receiver_type && f->short_name && + strcmp(f->receiver_type, receiver_qn) == 0 && + strcmp(f->short_name, method_name) == 0) { + if (!first_match) first_match = f; + int pc = count_func_params(f); + if (pc == arg_count) return f; // exact match + } + } + return first_match; // fallback to first match +} + +// --- Overload scoring by parameter type --- + +// Unwrap pointer/reference to get the core QN for comparison. +static const char* type_to_qn_simple(const CBMType* t) { + if (!t) return NULL; + // Unwrap references and pointers + while (t) { + switch (t->kind) { + case CBM_TYPE_POINTER: t = t->data.pointer.elem; continue; + case CBM_TYPE_REFERENCE: t = t->data.reference.elem; continue; + case CBM_TYPE_RVALUE_REF: t = t->data.reference.elem; continue; + case CBM_TYPE_NAMED: return t->data.named.qualified_name; + case CBM_TYPE_TEMPLATE: return t->data.template_type.template_name; + case CBM_TYPE_BUILTIN: return t->data.builtin.name; + default: return NULL; + } + } + return NULL; +} + +// Check if two types are compatible via implicit conversion. +static bool c_types_compatible(const char* expected_qn, const char* actual_qn) { + if (!expected_qn || !actual_qn) return false; + // const char* / char* -> std::string, std::string_view + if (strcmp(actual_qn, "char") == 0) { + if (strcmp(expected_qn, "std.string") == 0 || + strcmp(expected_qn, "std.basic_string") == 0 || + strcmp(expected_qn, "std.string_view") == 0 || + strcmp(expected_qn, "std.basic_string_view") == 0 || + strcmp(expected_qn, "absl.string_view") == 0) return true; + } + // Numeric promotions: all numeric builtins are interconvertible + static const char* numerics[] = {"int", "long", "short", "float", "double", + "unsigned", "size_t", "int8_t", "int16_t", "int32_t", "int64_t", + "uint8_t", "uint16_t", "uint32_t", "uint64_t", "ptrdiff_t", NULL}; + bool exp_numeric = false, act_numeric = false; + for (int i = 0; numerics[i]; i++) { + if (strcmp(expected_qn, numerics[i]) == 0) exp_numeric = true; + if (strcmp(actual_qn, numerics[i]) == 0) act_numeric = true; + } + if (exp_numeric && act_numeric) return true; + // bool <-> int + if ((strcmp(expected_qn, "bool") == 0 && act_numeric) || + (exp_numeric && strcmp(actual_qn, "bool") == 0)) return true; + return false; +} + +// Score an overload match: higher = better. 0 = wrong arg count (no match). +static int score_overload_match(const CBMRegisteredFunc* f, const CBMType** arg_types, int arg_count) { + int pc = count_func_params(f); + if (pc != arg_count) return 0; // wrong count = no match + if (!arg_types || !f->signature || !f->signature->data.func.param_types) return 50; + int score = 50; + for (int i = 0; i < arg_count; i++) { + const CBMType* expected = f->signature->data.func.param_types[i]; + const CBMType* actual = arg_types[i]; + if (!expected || !actual || cbm_type_is_unknown(actual)) continue; // neutral + const char* exp_qn = type_to_qn_simple(expected); + const char* act_qn = type_to_qn_simple(actual); + if (!exp_qn || !act_qn) continue; + if (strcmp(exp_qn, act_qn) == 0) { + score += 10; // exact type match + } else if (c_types_compatible(exp_qn, act_qn)) { + score += 5; // implicit conversion + } + } + return score; +} + +const CBMRegisteredFunc* cbm_registry_lookup_method_by_types(const CBMTypeRegistry* reg, + const char* receiver_qn, const char* method_name, + const CBMType** arg_types, int arg_count) { + if (!reg || !receiver_qn || !method_name) return NULL; + // If no type info, fall back to arg-count matching + if (!arg_types) return cbm_registry_lookup_method_by_args(reg, receiver_qn, method_name, arg_count); + + const CBMRegisteredFunc* best = NULL; + int best_score = 0; + const CBMRegisteredFunc* first_match = NULL; + + for (int i = 0; i < reg->func_count; i++) { + const CBMRegisteredFunc* f = ®->funcs[i]; + if (f->receiver_type && f->short_name && + strcmp(f->receiver_type, receiver_qn) == 0 && + strcmp(f->short_name, method_name) == 0) { + if (!first_match) first_match = f; + int s = score_overload_match(f, arg_types, arg_count); + if (s > best_score) { best_score = s; best = f; } + } + } + return best ? best : first_match; +} + +const CBMRegisteredFunc* cbm_registry_lookup_symbol_by_types(const CBMTypeRegistry* reg, + const char* package_qn, const char* name, + const CBMType** arg_types, int arg_count) { + if (!reg || !package_qn || !name) return NULL; + if (!arg_types) return cbm_registry_lookup_symbol_by_args(reg, package_qn, name, arg_count); + + size_t pkg_len = strlen(package_qn); + size_t name_len = strlen(name); + size_t total_len = pkg_len + 1 + name_len; + char buf[512]; + if (total_len >= sizeof(buf)) return NULL; + memcpy(buf, package_qn, pkg_len); + buf[pkg_len] = '.'; + memcpy(buf + pkg_len + 1, name, name_len); + buf[total_len] = '\0'; + + const CBMRegisteredFunc* best = NULL; + int best_score = 0; + const CBMRegisteredFunc* first_match = NULL; + + for (int i = 0; i < reg->func_count; i++) { + const CBMRegisteredFunc* f = ®->funcs[i]; + if (strcmp(f->qualified_name, buf) == 0) { + if (!first_match) first_match = f; + int s = score_overload_match(f, arg_types, arg_count); + if (s > best_score) { best_score = s; best = f; } + } + } + return best ? best : first_match; +} + +const CBMRegisteredFunc* cbm_registry_lookup_symbol_by_args(const CBMTypeRegistry* reg, + const char* package_qn, const char* name, int arg_count) { + if (!reg || !package_qn || !name) return NULL; + + size_t pkg_len = strlen(package_qn); + size_t name_len = strlen(name); + size_t total_len = pkg_len + 1 + name_len; + char buf[512]; + if (total_len >= sizeof(buf)) return NULL; + memcpy(buf, package_qn, pkg_len); + buf[pkg_len] = '.'; + memcpy(buf + pkg_len + 1, name, name_len); + buf[total_len] = '\0'; + + const CBMRegisteredFunc* first_match = NULL; + for (int i = 0; i < reg->func_count; i++) { + const CBMRegisteredFunc* f = ®->funcs[i]; + if (strcmp(f->qualified_name, buf) == 0) { + if (!first_match) first_match = f; + int pc = count_func_params(f); + if (pc == arg_count) return f; + } + } + return first_match; +} diff --git a/internal/cbm/lsp/type_registry.h b/internal/cbm/lsp/type_registry.h index aa1c33d63..6d0ae15ed 100644 --- a/internal/cbm/lsp/type_registry.h +++ b/internal/cbm/lsp/type_registry.h @@ -65,4 +65,31 @@ const CBMRegisteredFunc* cbm_registry_lookup_func(const CBMTypeRegistry* reg, const CBMRegisteredFunc* cbm_registry_lookup_symbol(const CBMTypeRegistry* reg, const char* package_qn, const char* name); +// Resolve type alias chain: follow alias_of until concrete type found (max 16 levels). +const CBMRegisteredType* cbm_registry_resolve_alias(const CBMTypeRegistry* reg, const char* type_qn); + +// Look up a method by receiver type QN + method name, following alias chains. +const CBMRegisteredFunc* cbm_registry_lookup_method_aliased(const CBMTypeRegistry* reg, + const char* receiver_qn, const char* method_name); + +// Look up a method by receiver type + name, preferring the overload with matching arg count. +// Falls back to any match if no exact arg count match found. +const CBMRegisteredFunc* cbm_registry_lookup_method_by_args(const CBMTypeRegistry* reg, + const char* receiver_qn, const char* method_name, int arg_count); + +// Look up a free function by package + name, preferring matching arg count. +const CBMRegisteredFunc* cbm_registry_lookup_symbol_by_args(const CBMTypeRegistry* reg, + const char* package_qn, const char* name, int arg_count); + +// Look up a method by receiver type + name, scoring overloads by parameter type match. +// arg_types may contain NULL entries for unknown types. Falls back to arg-count matching. +const CBMRegisteredFunc* cbm_registry_lookup_method_by_types(const CBMTypeRegistry* reg, + const char* receiver_qn, const char* method_name, + const CBMType** arg_types, int arg_count); + +// Look up a free function by package + name, scoring overloads by parameter type match. +const CBMRegisteredFunc* cbm_registry_lookup_symbol_by_types(const CBMTypeRegistry* reg, + const char* package_qn, const char* name, + const CBMType** arg_types, int arg_count); + #endif // CBM_LSP_TYPE_REGISTRY_H diff --git a/internal/cbm/lsp/type_rep.c b/internal/cbm/lsp/type_rep.c index 79f05a1d2..327f9d5e7 100644 --- a/internal/cbm/lsp/type_rep.c +++ b/internal/cbm/lsp/type_rep.c @@ -129,19 +129,71 @@ const CBMType* cbm_type_type_param(CBMArena* a, const char* name) { return t; } +const CBMType* cbm_type_reference(CBMArena* a, const CBMType* elem) { + CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType)); + if (!t) return &unknown_singleton; + memset(t, 0, sizeof(CBMType)); + t->kind = CBM_TYPE_REFERENCE; + t->data.reference.elem = elem; + return t; +} + +const CBMType* cbm_type_rvalue_ref(CBMArena* a, const CBMType* elem) { + CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType)); + if (!t) return &unknown_singleton; + memset(t, 0, sizeof(CBMType)); + t->kind = CBM_TYPE_RVALUE_REF; + t->data.reference.elem = elem; + return t; +} + +const CBMType* cbm_type_template(CBMArena* a, const char* name, const CBMType** args, int arg_count) { + CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType)); + if (!t) return &unknown_singleton; + memset(t, 0, sizeof(CBMType)); + t->kind = CBM_TYPE_TEMPLATE; + t->data.template_type.template_name = cbm_arena_strdup(a, name); + if (args && arg_count > 0) { + const CBMType** arr = (const CBMType**)cbm_arena_alloc(a, (arg_count + 1) * sizeof(const CBMType*)); + if (arr) { + for (int i = 0; i < arg_count; i++) arr[i] = args[i]; + arr[arg_count] = NULL; + t->data.template_type.template_args = arr; + } + } + t->data.template_type.arg_count = arg_count; + return t; +} + +const CBMType* cbm_type_alias(CBMArena* a, const char* alias_qn, const CBMType* underlying) { + CBMType* t = (CBMType*)cbm_arena_alloc(a, sizeof(CBMType)); + if (!t) return &unknown_singleton; + memset(t, 0, sizeof(CBMType)); + t->kind = CBM_TYPE_ALIAS; + t->data.alias.alias_qn = cbm_arena_strdup(a, alias_qn); + t->data.alias.underlying = underlying; + return t; +} + // Operations const CBMType* cbm_type_deref(const CBMType* t) { - if (!t || t->kind != CBM_TYPE_POINTER) return t; + if (!t) return t; + // Unwrap references transparently (C++ member access through refs) + if (t->kind == CBM_TYPE_REFERENCE || t->kind == CBM_TYPE_RVALUE_REF) + return t->data.reference.elem; + if (t->kind != CBM_TYPE_POINTER) return t; return t->data.pointer.elem; } const CBMType* cbm_type_elem(const CBMType* t) { if (!t) return cbm_type_unknown(); switch (t->kind) { - case CBM_TYPE_POINTER: return t->data.pointer.elem; - case CBM_TYPE_SLICE: return t->data.slice.elem; - case CBM_TYPE_CHANNEL: return t->data.channel.elem; + case CBM_TYPE_POINTER: return t->data.pointer.elem; + case CBM_TYPE_SLICE: return t->data.slice.elem; + case CBM_TYPE_CHANNEL: return t->data.channel.elem; + case CBM_TYPE_REFERENCE: return t->data.reference.elem; + case CBM_TYPE_RVALUE_REF: return t->data.reference.elem; default: return cbm_type_unknown(); } } @@ -158,6 +210,19 @@ bool cbm_type_is_pointer(const CBMType* t) { return t && t->kind == CBM_TYPE_POINTER; } +bool cbm_type_is_reference(const CBMType* t) { + return t && (t->kind == CBM_TYPE_REFERENCE || t->kind == CBM_TYPE_RVALUE_REF); +} + +const CBMType* cbm_type_resolve_alias(const CBMType* t) { + for (int i = 0; i < 16 && t; i++) { + if (t->kind != CBM_TYPE_ALIAS) return t; + if (!t->data.alias.underlying) return t; + t = t->data.alias.underlying; + } + return t; +} + // Generic substitution: recursively replace TYPE_PARAM with concrete types. const CBMType* cbm_type_substitute(CBMArena* a, const CBMType* t, const char** type_params, const CBMType** type_args) { @@ -174,6 +239,10 @@ const CBMType* cbm_type_substitute(CBMArena* a, const CBMType* t, } case CBM_TYPE_POINTER: return cbm_type_pointer(a, cbm_type_substitute(a, t->data.pointer.elem, type_params, type_args)); + case CBM_TYPE_REFERENCE: + return cbm_type_reference(a, cbm_type_substitute(a, t->data.reference.elem, type_params, type_args)); + case CBM_TYPE_RVALUE_REF: + return cbm_type_rvalue_ref(a, cbm_type_substitute(a, t->data.reference.elem, type_params, type_args)); case CBM_TYPE_SLICE: return cbm_type_slice(a, cbm_type_substitute(a, t->data.slice.elem, type_params, type_args)); case CBM_TYPE_MAP: diff --git a/internal/cbm/lsp/type_rep.h b/internal/cbm/lsp/type_rep.h index c70010ceb..f8ab0b57a 100644 --- a/internal/cbm/lsp/type_rep.h +++ b/internal/cbm/lsp/type_rep.h @@ -19,6 +19,10 @@ typedef enum { CBM_TYPE_BUILTIN, // int, string, bool, error, etc. CBM_TYPE_TUPLE, // multi-return (T1, T2) CBM_TYPE_TYPE_PARAM, // generic type parameter: T, K, V + CBM_TYPE_REFERENCE, // T& (C++ lvalue reference) + CBM_TYPE_RVALUE_REF, // T&& (C++ rvalue reference) + CBM_TYPE_TEMPLATE, // Parameterized type: vector — stores template name + args + CBM_TYPE_ALIAS, // Type alias: using/typedef — stores alias name + underlying type } CBMTypeKind; // Forward declaration @@ -58,6 +62,16 @@ struct CBMType { int count; } tuple; // TUPLE struct { const char* name; } type_param; // TYPE_PARAM + struct { const CBMType* elem; } reference; // REFERENCE / RVALUE_REF + struct { + const char* template_name; // "std::vector", "std::map" + const CBMType** template_args; // NULL-terminated + int arg_count; + } template_type; // TEMPLATE + struct { + const char* alias_qn; // "proj.ns.MyAlias" + const CBMType* underlying; // the actual type it aliases + } alias; // ALIAS } data; }; @@ -72,6 +86,10 @@ const CBMType* cbm_type_func(CBMArena* a, const char** param_names, const CBMTyp const CBMType* cbm_type_builtin(CBMArena* a, const char* name); const CBMType* cbm_type_tuple(CBMArena* a, const CBMType** elems, int count); const CBMType* cbm_type_type_param(CBMArena* a, const char* name); +const CBMType* cbm_type_reference(CBMArena* a, const CBMType* elem); +const CBMType* cbm_type_rvalue_ref(CBMArena* a, const CBMType* elem); +const CBMType* cbm_type_template(CBMArena* a, const char* name, const CBMType** args, int arg_count); +const CBMType* cbm_type_alias(CBMArena* a, const char* alias_qn, const CBMType* underlying); // Operations const CBMType* cbm_type_deref(const CBMType* t); // remove one pointer level @@ -79,6 +97,10 @@ const CBMType* cbm_type_elem(const CBMType* t); // get element type (s bool cbm_type_is_unknown(const CBMType* t); bool cbm_type_is_interface(const CBMType* t); bool cbm_type_is_pointer(const CBMType* t); +bool cbm_type_is_reference(const CBMType* t); + +// Follow alias chain with cycle detection (max 16 levels). +const CBMType* cbm_type_resolve_alias(const CBMType* t); // Generic type substitution: replace type params in t with concrete types. // type_params: NULL-terminated array of param names diff --git a/internal/cbm/lsp_all.c b/internal/cbm/lsp_all.c index 44c796c4d..178bb46ea 100644 --- a/internal/cbm/lsp_all.c +++ b/internal/cbm/lsp_all.c @@ -7,3 +7,6 @@ #include "lsp/type_registry.c" #include "lsp/go_lsp.c" #include "lsp/generated/go_stdlib_data.c" +#include "lsp/c_lsp.c" +#include "lsp/generated/c_stdlib_data.c" +#include "lsp/generated/cpp_stdlib_data.c" diff --git a/internal/cbm/lsp_bridge.go b/internal/cbm/lsp_bridge.go index 1fb22fe89..3a23ccbe4 100644 --- a/internal/cbm/lsp_bridge.go +++ b/internal/cbm/lsp_bridge.go @@ -7,6 +7,7 @@ package cbm #include "lsp/scope.h" #include "lsp/type_registry.h" #include "lsp/go_lsp.h" +#include "lsp/c_lsp.h" */ import "C" import ( @@ -153,6 +154,16 @@ func RunGoLSPCrossFile( // DefsToLSPDefs converts file-local Definition slice to CrossFileDef format. func DefsToLSPDefs(defs []Definition, moduleQN string) []CrossFileDef { result := make([]CrossFileDef, 0, len(defs)) + + // First pass: collect Field defs grouped by parent class QN → "name:type" pairs + fieldsByClass := make(map[string][]string) + for _, d := range defs { + if d.Label == "Field" && d.ParentClass != "" && d.Name != "" && d.ReturnType != "" { + fieldsByClass[d.ParentClass] = append(fieldsByClass[d.ParentClass], + d.Name+":"+d.ReturnType) + } + } + for _, d := range defs { if d.QualifiedName == "" || d.Name == "" { continue @@ -184,7 +195,6 @@ func DefsToLSPDefs(defs []Definition, moduleQN string) []CrossFileDef { IsInterface: d.Label == "Interface", } if len(d.BaseClasses) > 0 { - // Qualify embedded types relative to module embeds := make([]string, len(d.BaseClasses)) for i, bc := range d.BaseClasses { bc = strings.TrimLeft(bc, "*") @@ -192,6 +202,10 @@ func DefsToLSPDefs(defs []Definition, moduleQN string) []CrossFileDef { } cd.EmbeddedTypes = strings.Join(embeds, "|") } + // Attach field defs collected from Field-labeled definitions + if fields, ok := fieldsByClass[d.QualifiedName]; ok { + cd.FieldDefs = strings.Join(fields, "|") + } result = append(result, cd) } } @@ -217,3 +231,121 @@ func extractReceiverTypeQN(receiver, moduleQN string) string { } return moduleQN + "." + r } + +// RunCLSPCrossFile runs the C/C++ LSP type resolver with cross-file definitions. +// source is the file content, moduleQN is the file's module QN, +// cppMode enables C++ features (namespaces, classes, templates, etc.), +// fileDefs are the file's own definitions, crossDefs are from included headers, +// includes are the file's include mappings (header path → namespace QN). +func RunCLSPCrossFile( + source []byte, + moduleQN string, + cppMode bool, + fileDefs []CrossFileDef, + crossDefs []CrossFileDef, + includes []Import, +) []ResolvedCall { + if len(source) == 0 { + return nil + } + + Init() + + allDefs := make([]CrossFileDef, 0, len(fileDefs)+len(crossDefs)) + allDefs = append(allDefs, fileDefs...) + allDefs = append(allDefs, crossDefs...) + + if len(allDefs) == 0 { + return nil + } + + var arena C.CBMArena + C.cbm_arena_init(&arena) + defer C.cbm_arena_destroy(&arena) + + var toFree []unsafe.Pointer + defer func() { + for _, p := range toFree { + C.free(p) + } + }() + + cs := func(s string) *C.char { + if s == "" { + return nil + } + p := C.CString(s) + toFree = append(toFree, unsafe.Pointer(p)) + return p + } + + // Build C def array + nDefs := len(allDefs) + cDefsPtr := (*C.CBMLSPDef)(C.malloc(C.size_t(nDefs) * C.size_t(unsafe.Sizeof(C.CBMLSPDef{})))) + if cDefsPtr == nil { + return nil + } + toFree = append(toFree, unsafe.Pointer(cDefsPtr)) + cDefs := unsafe.Slice(cDefsPtr, nDefs) + + for i, d := range allDefs { + cDefs[i] = C.CBMLSPDef{ + qualified_name: cs(d.QualifiedName), + short_name: cs(d.ShortName), + label: cs(d.Label), + receiver_type: cs(d.ReceiverType), + def_module_qn: cs(d.DefModuleQN), + return_types: cs(d.ReturnTypes), + embedded_types: cs(d.EmbeddedTypes), + field_defs: cs(d.FieldDefs), + method_names_str: cs(d.MethodNames), + is_interface: C.bool(d.IsInterface), + } + } + + // Build include arrays (header path → namespace QN) + nIncludes := len(includes) + ptrSize := C.size_t(unsafe.Sizeof((*C.char)(nil))) + cIncPaths := (**C.char)(C.malloc(C.size_t(nIncludes+1) * ptrSize)) + cIncQNs := (**C.char)(C.malloc(C.size_t(nIncludes+1) * ptrSize)) + toFree = append(toFree, unsafe.Pointer(cIncPaths), unsafe.Pointer(cIncQNs)) + + incPathSlice := unsafe.Slice(cIncPaths, nIncludes+1) + incQNSlice := unsafe.Slice(cIncQNs, nIncludes+1) + for i, inc := range includes { + incPathSlice[i] = cs(inc.LocalName) + incQNSlice[i] = cs(inc.ModulePath) + } + incPathSlice[nIncludes] = nil + incQNSlice[nIncludes] = nil + + cModuleQN := cs(moduleQN) + cSource := (*C.char)(unsafe.Pointer(&source[0])) + + var outCalls C.CBMResolvedCallArray + C.cbm_run_c_lsp_cross( + &arena, + cSource, C.int(len(source)), + cModuleQN, + C.bool(cppMode), + cDefsPtr, C.int(nDefs), + cIncPaths, cIncQNs, C.int(nIncludes), + &outCalls, + ) + + if outCalls.count == 0 { + return nil + } + result := make([]ResolvedCall, outCalls.count) + rcs := unsafe.Slice(outCalls.items, outCalls.count) + for i, rc := range rcs { + result[i] = ResolvedCall{ + CallerQN: C.GoString(rc.caller_qn), + CalleeQN: C.GoString(rc.callee_qn), + Strategy: C.GoString(rc.strategy), + Confidence: float32(rc.confidence), + Reason: goStringOrEmpty(rc.reason), + } + } + return result +} diff --git a/internal/cbm/lsp_c_test.go b/internal/cbm/lsp_c_test.go new file mode 100644 index 000000000..83b794108 --- /dev/null +++ b/internal/cbm/lsp_c_test.go @@ -0,0 +1,2970 @@ +package cbm + +import ( + "strings" + "testing" + + "github.com/DeusData/codebase-memory-mcp/internal/lang" +) + +// extractCWithRegistry extracts a C file and returns the result. +func extractCWithRegistry(t *testing.T, source string) *FileResult { + t.Helper() + result, err := ExtractFile([]byte(source), lang.C, "test", "main.c") + if err != nil { + t.Fatalf("ExtractFile failed: %v", err) + } + return result +} + +// extractCPPWithRegistry extracts a C++ file and returns the result. +func extractCPPWithRegistry(t *testing.T, source string) *FileResult { + t.Helper() + result, err := ExtractFile([]byte(source), lang.CPP, "test", "main.cpp") + if err != nil { + t.Fatalf("ExtractFile failed: %v", err) + } + return result +} + +// ============================================================================ +// Test Category 1: Simple variable declarations and method calls +// ============================================================================ + +func TestCLSP_SimpleVarDecl(t *testing.T) { + source := ` +struct Foo { + int value; +}; + +int bar(struct Foo* f); + +void baz() { + struct Foo x; + bar(&x); +} +` + result := extractCWithRegistry(t, source) + requireResolvedCall(t, result, "baz", "bar") +} + +func TestCLSP_PointerArrow(t *testing.T) { + source := ` +class Foo { +public: + int bar() { return 0; } +}; + +void test(Foo* p) { + p->bar(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "bar") +} + +func TestCLSP_DotAccess(t *testing.T) { + source := ` +class Foo { +public: + int bar() { return 0; } +}; + +void test() { + Foo x; + x.bar(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "bar") +} + +// ============================================================================ +// Test Category 2: Auto type inference +// ============================================================================ + +func TestCLSP_AutoInference(t *testing.T) { + source := ` +class Foo { +public: + int bar() { return 0; } +}; + +Foo createFoo() { return Foo(); } + +void test() { + auto x = createFoo(); + x.bar(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "bar") +} + +// ============================================================================ +// Test Category 3: Namespace-qualified calls +// ============================================================================ + +func TestCLSP_NamespaceQualified(t *testing.T) { + source := ` +namespace ns { + class Foo { + public: + static int staticMethod() { return 0; } + }; +} + +void test() { + ns::Foo::staticMethod(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "staticMethod") +} + +// ============================================================================ +// Test Category 4: Constructor calls +// ============================================================================ + +func TestCLSP_Constructor(t *testing.T) { + source := ` +class Foo { +public: + Foo(int a, int b) {} + int bar() { return 0; } +}; + +void test() { + Foo x(1, 2); + x.bar(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "bar") +} + +func TestCLSP_NewDelete(t *testing.T) { + source := ` +class Foo { +public: + int bar() { return 0; } +}; + +void test() { + Foo* p = new Foo(); + p->bar(); + delete p; +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "bar") +} + +// ============================================================================ +// Test Category 5: Implicit this +// ============================================================================ + +func TestCLSP_ImplicitThis(t *testing.T) { + source := ` +class Foo { +public: + int helper() { return 0; } + void doWork() { + helper(); + } +}; +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "doWork", "helper") + if rc == nil { + t.Log("implicit this resolution not yet detected — may need method body scoping") + // This is acceptable — implicit this is a stretch goal + } +} + +func TestCLSP_ExplicitThis(t *testing.T) { + source := ` +class Foo { +public: + int bar() { return 0; } + void doWork() { + this->bar(); + } +}; +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "doWork", "bar") +} + +// ============================================================================ +// Test Category 6: Type aliases +// ============================================================================ + +func TestCLSP_TypeAlias(t *testing.T) { + source := ` +class Foo { +public: + int bar() { return 0; } +}; + +using MyFoo = Foo; + +void test() { + MyFoo x; + x.bar(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "bar") +} + +func TestCLSP_Typedef(t *testing.T) { + source := ` +class Foo { +public: + int bar() { return 0; } +}; + +typedef Foo MyFoo; + +void test() { + MyFoo x; + x.bar(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "bar") +} + +// ============================================================================ +// Test Category 7: Scope chain +// ============================================================================ + +func TestCLSP_ScopeChain(t *testing.T) { + source := ` +class Foo { +public: + int method1() { return 0; } +}; + +class Bar { +public: + int method2() { return 0; } +}; + +void test() { + { + Foo x; + x.method1(); + } + { + Bar x; + x.method2(); + } +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "method1") + requireResolvedCall(t, result, "test", "method2") +} + +// ============================================================================ +// Test Category 8: Cast expressions +// ============================================================================ + +func TestCLSP_StaticCast(t *testing.T) { + source := ` +class Base { +public: + virtual int bar() { return 0; } +}; + +class Derived : public Base { +public: + int bar() override { return 1; } + int extra() { return 2; } +}; + +void test(Base* b) { + static_cast(b)->extra(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "extra") +} + +// ============================================================================ +// Test Category 9: Using namespace +// ============================================================================ + +func TestCLSP_UsingNamespace(t *testing.T) { + source := ` +namespace ns { + int foo() { return 42; } +} + +void test() { + using namespace ns; + foo(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "foo") +} + +// ============================================================================ +// Test Category 10: C mode (pure C, no classes) +// ============================================================================ + +func TestCLSP_CMode(t *testing.T) { + source := ` +#include + +struct Point { + int x; + int y; +}; + +int compute(struct Point* p) { + return p->x + p->y; +} + +void test() { + struct Point p; + p.x = 1; + p.y = 2; + compute(&p); +} +` + result := extractCWithRegistry(t, source) + requireResolvedCall(t, result, "test", "compute") +} + +// ============================================================================ +// Test Category 11: Direct function calls +// ============================================================================ + +func TestCLSP_DirectCall(t *testing.T) { + source := ` +int helper(int x) { return x + 1; } + +void test() { + helper(42); +} +` + result := extractCWithRegistry(t, source) + requireResolvedCall(t, result, "test", "helper") +} + +func TestCLSP_DirectCallCPP(t *testing.T) { + source := ` +int helper(int x) { return x + 1; } + +void test() { + helper(42); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "helper") +} + +// ============================================================================ +// Test Category 12: Stdlib calls +// ============================================================================ + +func TestCLSP_StdlibCall(t *testing.T) { + source := ` +#include + +void test() { + char buf[100]; + strlen(buf); +} +` + result := extractCWithRegistry(t, source) + // strlen is registered in stdlib, should be resolvable + rc := findResolvedCall(t, result, "test", "strlen") + if rc != nil { + if rc.Confidence < 0.5 { + t.Errorf("expected high confidence for strlen, got %.2f", rc.Confidence) + } + } +} + +// ============================================================================ +// Test Category 13: Multiple resolved calls in same function +// ============================================================================ + +func TestCLSP_MultipleCallsSameFunc(t *testing.T) { + source := ` +class Logger { +public: + void info(const char* msg) {} + void error(const char* msg) {} +}; + +class Config { +public: + const char* get(const char* key) { return ""; } +}; + +void setup(Logger* log, Config* cfg) { + log->info("starting"); + cfg->get("port"); + log->error("failed"); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "setup", "info") + requireResolvedCall(t, result, "setup", "get") + requireResolvedCall(t, result, "setup", "error") +} + +// ============================================================================ +// Test Category 14: Return type chain +// ============================================================================ + +func TestCLSP_ReturnTypeChain(t *testing.T) { + source := ` +class File { +public: + int read() { return 0; } +}; + +File* open(const char* path) { return nullptr; } + +void test() { + File* f = open("test.txt"); + f->read(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "read") +} + +// ============================================================================ +// Test Category 15: Method chaining +// ============================================================================ + +func TestCLSP_MethodChaining(t *testing.T) { + source := ` +class Builder { +public: + Builder& setName(const char* name) { return *this; } + Builder& setValue(int val) { return *this; } + void build() {} +}; + +void test() { + Builder b; + b.setName("foo").setValue(42).build(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "setName") + // Method chaining through return type + rc := findResolvedCall(t, result, "test", "build") + if rc == nil { + t.Log("method chaining through return type not resolved — acceptable limitation") + } +} + +// ============================================================================ +// Test Category 16: Inheritance +// ============================================================================ + +func TestCLSP_Inheritance(t *testing.T) { + source := ` +class Base { +public: + int baseMethod() { return 0; } +}; + +class Derived : public Base { +public: + int derivedMethod() { return 1; } +}; + +void test() { + Derived d; + d.derivedMethod(); + d.baseMethod(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "derivedMethod") + // Base class method through inheritance — may or may not resolve + rc := findResolvedCall(t, result, "test", "baseMethod") + if rc == nil { + t.Log("base class method through inheritance not resolved — needs cross-file enrichment") + } +} + +// ============================================================================ +// Test Category 17: Operator overloads +// ============================================================================ + +func TestCLSP_OperatorStream(t *testing.T) { + source := ` +#include + +void test() { + int x = 42; +} +` + // Just verify it doesn't crash on operator overload nodes + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } +} + +// ============================================================================ +// Test Category 18: Cross-file resolution +// ============================================================================ + +func TestCLSP_CrossFile(t *testing.T) { + source := ` +class Widget { +public: + void render() {} +}; + +void test() { + Widget w; + w.render(); +} +` + fileDefs := []CrossFileDef{ + { + QualifiedName: "test.main-cpp.Widget", + ShortName: "Widget", + Label: "Class", + DefModuleQN: "test.main-cpp", + }, + { + QualifiedName: "test.main-cpp.Widget.render", + ShortName: "render", + Label: "Method", + ReceiverType: "test.main-cpp.Widget", + DefModuleQN: "test.main-cpp", + }, + } + + crossDefs := []CrossFileDef{ + { + QualifiedName: "test.helper-cpp.Helper.process", + ShortName: "process", + Label: "Method", + ReceiverType: "test.helper-cpp.Helper", + DefModuleQN: "test.helper-cpp", + }, + } + + resolved := RunCLSPCrossFile( + []byte(source), + "test.main-cpp", + true, // cpp mode + fileDefs, + crossDefs, + nil, // no includes needed for this test + ) + + if len(resolved) == 0 { + t.Log("cross-file resolution returned no calls — may need parser re-parse support") + return + } + + found := false + for _, rc := range resolved { + if contains(rc.CalleeQN, "render") { + found = true + break + } + } + if !found { + t.Error("expected cross-file resolution to find render() call") + for _, rc := range resolved { + t.Logf(" %s -> %s [%s %.2f]", rc.CallerQN, rc.CalleeQN, rc.Strategy, rc.Confidence) + } + } +} + +// ============================================================================ +// Test Category 19: Verify no crashes on various patterns +// ============================================================================ + +func TestCLSP_NocrashTemplateExpression(t *testing.T) { + source := ` +#include +#include + +void test() { + int x = 42; + double y = 3.14; + const char* s = "hello"; +} +` + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } +} + +func TestCLSP_NocrashLambda(t *testing.T) { + source := ` +void test() { + auto f = [](int x) -> int { return x + 1; }; + f(42); +} +` + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } +} + +func TestCLSP_NocrashNestedNamespace(t *testing.T) { + source := ` +namespace a { + namespace b { + namespace c { + void deep() {} + } + } +} + +void test() { + a::b::c::deep(); +} +` + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } +} + +func TestCLSP_NocrashEmptySource(t *testing.T) { + result, err := ExtractFile([]byte(""), lang.CPP, "test", "empty.cpp") + if err != nil { + t.Fatalf("ExtractFile failed: %v", err) + } + if result == nil { + t.Fatal("expected non-nil result for empty source") + } +} + +func TestCLSP_NocrashComplexClass(t *testing.T) { + source := ` +class Base { +public: + virtual ~Base() {} + virtual void process() = 0; +}; + +class Derived : public Base { + int data_; +public: + Derived(int d) : data_(d) {} + void process() override { + data_++; + } + int getData() const { return data_; } +}; + +template +class Container { + T* items_; + int count_; +public: + Container() : items_(nullptr), count_(0) {} + void add(const T& item) { count_++; } + int size() const { return count_; } +}; + +void test() { + Derived d(42); + d.process(); + d.getData(); +} +` + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } + requireResolvedCall(t, result, "test", "process") + requireResolvedCall(t, result, "test", "getData") +} + +// ============================================================================ +// Test Category 20: Operator overloads +// ============================================================================ + +func TestCLSP_OperatorSubscript(t *testing.T) { + source := ` +class Vec { +public: + int& operator[](int idx) { static int x; return x; } +}; + +void test() { + Vec v; + v[0]; +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "operator[]") +} + +func TestCLSP_OperatorBinary(t *testing.T) { + source := ` +class Vec3 { +public: + Vec3 operator+(const Vec3& other) { return Vec3(); } +}; + +void test() { + Vec3 a; + Vec3 b; + a + b; +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "operator+") +} + +func TestCLSP_OperatorUnary(t *testing.T) { + source := ` +class Iter { +public: + int operator*() { return 0; } + Iter& operator++() { return *this; } +}; + +void test() { + Iter it; + *it; + ++it; +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "operator*") + requireResolvedCall(t, result, "test", "operator++") +} + +// ============================================================================ +// Test Category 21: Functor (operator()) +// ============================================================================ + +func TestCLSP_Functor(t *testing.T) { + source := ` +class Predicate { +public: + bool operator()(int x) { return x > 0; } +}; + +void test() { + Predicate pred; + pred(42); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "operator()") +} + +// ============================================================================ +// Test Category 22: Copy/move constructor +// ============================================================================ + +func TestCLSP_CopyConstructor(t *testing.T) { + source := ` +class Foo { +public: + Foo() {} + Foo(const Foo& other) {} + int bar() { return 0; } +}; + +void test() { + Foo a; + Foo b = a; +} +` + result := extractCPPWithRegistry(t, source) + // Should detect copy constructor call + rc := findResolvedCall(t, result, "test", "Foo") + if rc != nil && contains(rc.Strategy, "copy_constructor") { + t.Log("copy constructor correctly detected") + } +} + +// ============================================================================ +// Test Category 23: Delete expression (destructor) +// ============================================================================ + +func TestCLSP_DeleteDestructor(t *testing.T) { + source := ` +class Widget { +public: + ~Widget() {} +}; + +void test() { + Widget* w = new Widget(); + delete w; +} +` + result := extractCPPWithRegistry(t, source) + // Should emit constructor for new and destructor for delete + rc := findResolvedCall(t, result, "test", "Widget") + if rc == nil { + t.Log("constructor/destructor not detected") + } +} + +// ============================================================================ +// Test Category 24: Range-for type deduction +// ============================================================================ + +func TestCLSP_RangeFor(t *testing.T) { + source := ` +class Foo { +public: + int bar() { return 0; } +}; + +void test() { + Foo arr[3]; + for (auto& x : arr) { + x.bar(); + } +} +` + // This may or may not resolve depending on array element type deduction + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } +} + +// ============================================================================ +// Test Category 25: Parent namespace traversal +// ============================================================================ + +func TestCLSP_ParentNamespace(t *testing.T) { + source := ` +namespace outer { + int helper() { return 42; } + + namespace inner { + void test() { + helper(); + } + } +} +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "test", "helper") + if rc == nil { + t.Log("parent namespace traversal not resolving — acceptable") + } +} + +// ============================================================================ +// Test Category 26: Conversion operators +// ============================================================================ + +func TestCLSP_ConversionOperatorBool(t *testing.T) { + source := ` +class Guard { +public: + operator bool() { return true; } +}; + +void test() { + Guard g; + if (g) { + // implicit operator bool call + } +} +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "test", "operator bool") + if rc == nil { + t.Log("implicit operator bool() not detected — acceptable limitation") + } +} + +// ============================================================================ +// Test Category 27: Namespace alias +// ============================================================================ + +func TestCLSP_NamespaceAlias(t *testing.T) { + source := ` +namespace very_long_name { + int foo() { return 42; } +} + +void test() { + namespace vln = very_long_name; + vln::foo(); +} +` + result := extractCPPWithRegistry(t, source) + // Namespace alias resolution in function scope + if result == nil { + t.Fatal("extraction returned nil") + } +} + +// ============================================================================ +// Test Category 28: Template in namespace +// ============================================================================ + +func TestCLSP_TemplateInNamespace(t *testing.T) { + source := ` +namespace ns { + template + class Wrapper { + public: + T get() { return T(); } + }; + + template + void process(T val) {} +} + +void test() { + int x = 42; +} +` + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil for template in namespace") + } +} + +// ============================================================================ +// Test Category 29: No crash on various edge cases +// ============================================================================ + +func TestCLSP_NocrashUsingEnum(t *testing.T) { + source := ` +enum class Color { Red, Green, Blue }; + +void test() { + Color c = Color::Red; +} +` + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } +} + +func TestCLSP_NocrashMultipleInheritance(t *testing.T) { + source := ` +class A { +public: + void methodA() {} +}; + +class B { +public: + void methodB() {} +}; + +class C : public A, public B { +public: + void methodC() {} +}; + +void test() { + C c; + c.methodC(); + c.methodA(); + c.methodB(); +} +` + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } + requireResolvedCall(t, result, "test", "methodC") +} + +func TestCLSP_NocrashPointerArithmetic(t *testing.T) { + source := ` +void test() { + int arr[10]; + int* p = arr; + *(p + 3) = 42; +} +` + result := extractCWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } +} + +// ============================================================================ +// Test Category 30: Function pointer resolution +// ============================================================================ + +func TestCLSP_FunctionPointer(t *testing.T) { + source := ` +int target_func(int x) { return x + 1; } + +void test() { + int (*fp)(int) = &target_func; + fp(42); +} +` + result := extractCWithRegistry(t, source) + requireResolvedCall(t, result, "test", "target_func") +} + +func TestCLSP_FunctionPointerDecay(t *testing.T) { + source := ` +int target_func(int x) { return x + 1; } + +void test() { + int (*fp)(int) = target_func; + fp(42); +} +` + result := extractCWithRegistry(t, source) + requireResolvedCall(t, result, "test", "target_func") +} + +// ============================================================================ +// Test Category 31: Overloaded function call (arg count matching) +// ============================================================================ + +func TestCLSP_OverloadByArgCount(t *testing.T) { + source := ` +class Foo { +public: + int bar() { return 0; } + int bar(int x) { return x; } + int bar(int x, int y) { return x + y; } +}; + +void test() { + Foo f; + f.bar(); + f.bar(1); + f.bar(1, 2); +} +` + result := extractCPPWithRegistry(t, source) + // Should resolve all three overloads + requireResolvedCall(t, result, "test", "bar") +} + +// ============================================================================ +// Test Category 32: Template default args +// ============================================================================ + +func TestCLSP_TemplateDefaultArgs(t *testing.T) { + source := ` +class DefaultType { +public: + int method() { return 0; } +}; + +template +void process() { + T obj; + obj.method(); +} +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "process", "method") + if rc == nil { + t.Log("template default arg resolution not working — tree-sitter may not expose default_type field") + } +} + +// ============================================================================ +// Test Category 33: C++20 spaceship operator +// ============================================================================ + +func TestCLSP_SpaceshipOperator(t *testing.T) { + source := ` +class Vec3 { +public: + int x, y, z; + bool operator==(const Vec3& other) { return x == other.x; } +}; + +void test() { + Vec3 a; + Vec3 b; + a == b; +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "operator==") +} + +// ============================================================================ +// Test Category 34: C++20 concepts (no crash) +// ============================================================================ + +func TestCLSP_NocrashConcept(t *testing.T) { + source := ` +template +class Container { +public: + void push(T val) {} + int size() { return 0; } +}; + +void test() { + Container c; + c.push(42); + c.size(); +} +` + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } + requireResolvedCall(t, result, "test", "push") + requireResolvedCall(t, result, "test", "size") +} + +// ============================================================================ +// Test Category 35: Dependent member access via template default +// ============================================================================ + +func TestCLSP_DependentMemberAccess(t *testing.T) { + source := ` +class Widget { +public: + void render() {} +}; + +template +void draw(T& obj) { + obj.render(); +} +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "draw", "render") + if rc == nil { + t.Log("dependent member access through template default not resolved — acceptable") + } +} + +func TestCLSP_NocrashTryCatch(t *testing.T) { + source := ` +class Exception { +public: + const char* what() { return "error"; } +}; + +void test() { + try { + throw Exception(); + } catch (Exception& e) { + e.what(); + } +} +` + result := extractCPPWithRegistry(t, source) + if result == nil { + t.Fatal("extraction returned nil") + } +} + +// ============================================================================ +// Test Category: Macro expansion via simplecpp preprocessor (dual-parse) +// ============================================================================ + +// hasRawCall checks if any raw Call has the given callee name. +func hasRawCall(result *FileResult, calleeName string) bool { + for _, c := range result.Calls { + if c.CalleeName == calleeName { + return true + } + } + return false +} + +func TestCLSP_MacroWrappedCall(t *testing.T) { + source := ` +#define CALL(f) f() +void foo(void); +void test(void) { CALL(foo); } +` + result := extractCWithRegistry(t, source) + if !hasRawCall(result, "foo") { + t.Errorf("expected raw call to 'foo' from macro expansion, got calls: %v", result.Calls) + } +} + +func TestCLSP_MacroWithArgs(t *testing.T) { + source := ` +int printf(const char* fmt, ...); +#define LOG(msg) printf(msg) +void test(void) { LOG("hi"); } +` + result := extractCWithRegistry(t, source) + if !hasRawCall(result, "printf") { + t.Errorf("expected raw call to 'printf' from macro expansion, got calls: %v", result.Calls) + } +} + +func TestCLSP_RecursiveMacro(t *testing.T) { + source := ` +void target(int x); +#define B(x) target(x) +#define A(x) B(x) +void test(void) { A(1); } +` + result := extractCWithRegistry(t, source) + if !hasRawCall(result, "target") { + t.Errorf("expected raw call to 'target' from recursive macro, got calls: %v", result.Calls) + } +} + +func TestCLSP_ConditionalMacro(t *testing.T) { + source := ` +void new_func(void); +void old_func(void); +#define USE_NEW 1 +#ifdef USE_NEW +void test(void) { new_func(); } +#else +void test(void) { old_func(); } +#endif +` + result := extractCWithRegistry(t, source) + if !hasRawCall(result, "new_func") { + t.Errorf("expected call to 'new_func' from #ifdef branch, got calls: %v", result.Calls) + } +} + +func TestCLSP_TokenPaste(t *testing.T) { + source := ` +void order_handler(void); +#define HANDLER(name) name##_handler() +void test(void) { HANDLER(order); } +` + result := extractCWithRegistry(t, source) + if !hasRawCall(result, "order_handler") { + t.Errorf("expected raw call to 'order_handler' from ## paste, got calls: %v", result.Calls) + } +} + +func TestCLSP_NoMacroNoOverhead(t *testing.T) { + // Pure C without #define — preprocessor should return NULL (fast path). + source := ` +void foo(void); +void bar(void); +void test(void) { foo(); bar(); } +` + result := extractCWithRegistry(t, source) + if !hasRawCall(result, "foo") || !hasRawCall(result, "bar") { + t.Errorf("expected calls to 'foo' and 'bar', got: %v", result.Calls) + } +} + +func TestCLSP_VariadicMacro(t *testing.T) { + source := ` +int fprintf(void* stream, const char* fmt, ...); +#define DBG(fmt, ...) fprintf(0, fmt, __VA_ARGS__) +void test(void) { DBG("x=%d", 42); } +` + result := extractCWithRegistry(t, source) + if !hasRawCall(result, "fprintf") { + t.Errorf("expected raw call to 'fprintf' from variadic macro, got calls: %v", result.Calls) + } +} + +func TestCLSP_CPPMacroMethodCall(t *testing.T) { + source := ` +class Logger { +public: + void log(const char* msg) {} +}; + +Logger* getLogger(); +#define LOG(msg) getLogger()->log(msg) + +void test() { + LOG("hello"); +} +` + result := extractCPPWithRegistry(t, source) + if !hasRawCall(result, "getLogger") { + t.Errorf("expected raw call to 'getLogger' from macro expansion, got calls: %v", result.Calls) + } +} + +// ============================================================================ +// Test Category: Struct field extraction +// ============================================================================ + +func TestCLSP_StructFieldExtraction(t *testing.T) { + source := ` +struct Point { + int x; + int y; + float z; +}; +` + result := extractCWithRegistry(t, source) + // Check that Field definitions are extracted + fieldCount := 0 + fieldNames := map[string]string{} // name → return_type + for _, d := range result.Definitions { + if d.Label == "Field" { + fieldCount++ + fieldNames[d.Name] = d.ReturnType + } + } + if fieldCount != 3 { + t.Errorf("expected 3 Field defs, got %d", fieldCount) + for _, d := range result.Definitions { + t.Logf(" def: %s label=%s type=%s", d.Name, d.Label, d.ReturnType) + } + } + for _, name := range []string{"x", "y", "z"} { + if _, ok := fieldNames[name]; !ok { + t.Errorf("expected field %q to be extracted", name) + } + } + if fieldNames["x"] != "int" { + t.Errorf("expected field x type 'int', got %q", fieldNames["x"]) + } + if fieldNames["z"] != "float" { + t.Errorf("expected field z type 'float', got %q", fieldNames["z"]) + } +} + +func TestCLSP_StructFieldDefsToLSPDefs(t *testing.T) { + source := ` +struct Config { + int timeout; + char* name; + void (*callback)(int); +}; +` + result := extractCWithRegistry(t, source) + + // Convert to LSP defs + lspDefs := DefsToLSPDefs(result.Definitions, "test.main_c") + var configDef *CrossFileDef + for i := range lspDefs { + if lspDefs[i].ShortName == "Config" && lspDefs[i].Label == "Class" { + configDef = &lspDefs[i] + break + } + } + if configDef == nil { + t.Fatal("Config class def not found in LSP defs") + } + // FieldDefs should contain at least timeout:int and name:char + if configDef.FieldDefs == "" { + t.Errorf("expected FieldDefs to be populated, got empty string") + for _, d := range result.Definitions { + t.Logf(" def: %s label=%s type=%s parent=%s", d.Name, d.Label, d.ReturnType, d.ParentClass) + } + } + // Function pointer field (callback) should NOT be in FieldDefs (it's a method) + if configDef.FieldDefs != "" { + t.Logf("FieldDefs: %s", configDef.FieldDefs) + } +} + +func TestCLSP_MakeSharedTemplateArg(t *testing.T) { + source := ` +#include + +class Widget { +public: + void resize(int w, int h) {} +}; + +void test() { + auto ptr = std::make_shared(); + ptr->resize(10, 20); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "resize") +} + +func TestCLSP_MakeUniqueTemplateArg(t *testing.T) { + source := ` +#include + +class Engine { +public: + void start() {} +}; + +void test() { + auto e = std::make_unique(); + e->start(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "start") +} + +func TestCLSP_TemplateClassMethodReturnType(t *testing.T) { + source := ` +template +class Box { +public: + T get() { return val; } + void set(T v) { val = v; } +private: + T val; +}; + +class Widget { +public: + void draw() {} +}; + +void test() { + Box b; + b.get().draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_TrailingReturnType(t *testing.T) { + source := ` +class Foo { +public: + void bar() {} +}; + +auto createFoo() -> Foo* { + return new Foo(); +} + +void test() { + auto f = createFoo(); + f->bar(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "bar") +} + +func TestCLSP_TrailingReturnTypeMethod(t *testing.T) { + source := ` +class Builder { +public: + auto self() -> Builder& { return *this; } + void build() {} +}; + +void test() { + Builder b; + b.self().build(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "build") +} + +func TestCLSP_CPPClassFieldExtraction(t *testing.T) { + source := ` +class Widget { +public: + int width; + int height; + void resize(int w, int h) {} +private: + float scale; +}; +` + result := extractCPPWithRegistry(t, source) + fieldCount := 0 + methodCount := 0 + for _, d := range result.Definitions { + if d.Label == "Field" { + fieldCount++ + } + if d.Label == "Method" { + methodCount++ + } + } + if fieldCount != 3 { + t.Errorf("expected 3 Field defs (width, height, scale), got %d", fieldCount) + for _, d := range result.Definitions { + t.Logf(" def: %s label=%s type=%s", d.Name, d.Label, d.ReturnType) + } + } + if methodCount != 1 { + t.Errorf("expected 1 Method def (resize), got %d", methodCount) + } +} + +// --- STL stub coverage tests --- + +func TestCLSP_StdVariant(t *testing.T) { + source := ` +#include +#include + +void test() { + std::variant v; + v.index(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "index") +} + +func TestCLSP_StdDeque(t *testing.T) { + source := ` +#include + +class Task { +public: + void run() {} +}; + +void test() { + std::deque q; + q.push_back(Task()); + q.front().run(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "push_back") + requireResolvedCall(t, result, "test", "front") + requireResolvedCall(t, result, "test", "run") +} + +func TestCLSP_StdFilesystem(t *testing.T) { + source := ` +#include + +void test() { + std::filesystem::path p("/tmp/test"); + p.filename(); + std::filesystem::exists(p); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "filename") + requireResolvedCall(t, result, "test", "exists") +} + +func TestCLSP_StdAccumulate(t *testing.T) { + source := ` +#include +#include + +void test() { + std::vector v; + std::accumulate(v.begin(), v.end(), 0); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "accumulate") + requireResolvedCall(t, result, "test", "begin") + requireResolvedCall(t, result, "test", "end") +} + +func TestCLSP_StdStringStream(t *testing.T) { + source := ` +#include +#include + +void test() { + std::stringstream ss; + ss.str(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "str") +} + +// --- Third-party stub coverage tests --- + +func TestCLSP_AbseilStatusOr(t *testing.T) { + source := ` +namespace absl { + class Status { + public: + bool ok() { return true; } + int code() { return 0; } + }; + template class StatusOr { + public: + bool ok() { return true; } + T value() { return T(); } + Status status() { return Status(); } + }; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + absl::StatusOr result; + if (result.ok()) { + result.value().draw(); + } +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "ok") + requireResolvedCall(t, result, "test", "value") + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_SpdlogLogger(t *testing.T) { + source := ` +namespace spdlog { + class logger { + public: + void info(const char* msg) {} + void warn(const char* msg) {} + void error(const char* msg) {} + }; + void info(const char* msg) {} +} + +void test() { + spdlog::logger log; + log.info("hello"); + log.warn("caution"); + spdlog::info("global"); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "info") + requireResolvedCall(t, result, "test", "warn") +} + +func TestCLSP_QtQString(t *testing.T) { + source := ` +class QString { +public: + int length() { return 0; } + bool isEmpty() { return true; } + QString trimmed() { return *this; } + const char* toUtf8() { return ""; } +}; + +void test() { + QString s; + s.trimmed().length(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "trimmed") + requireResolvedCall(t, result, "test", "length") +} + +// --- ADL (Argument-Dependent Lookup) tests --- + +func TestCLSP_ADL_Swap(t *testing.T) { + source := ` +namespace mylib { + class Widget { + public: + void draw() {} + }; + void swap(Widget& a, Widget& b) {} +} + +void test() { + mylib::Widget a, b; + swap(a, b); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "swap") +} + +func TestCLSP_ADL_OperatorFreeFunc(t *testing.T) { + source := ` +namespace geo { + class Point { + public: + int x, y; + }; + double distance(Point& a, Point& b) { return 0.0; } +} + +void test() { + geo::Point p1, p2; + distance(p1, p2); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "distance") +} + +func TestCLSP_ADL_StdSort(t *testing.T) { + // std::sort with std::vector iterators — ADL should find std::sort + source := ` +#include +#include + +void test() { + std::vector v; + sort(v.begin(), v.end()); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "sort") +} + +func TestCLSP_ADL_NoFalsePositive(t *testing.T) { + // Non-namespace type should NOT trigger ADL for missing functions + source := ` +class Foo { +public: + int x; +}; + +void test() { + Foo f; + unknown_func(f); +} +` + result := extractCPPWithRegistry(t, source) + // Should NOT resolve — Foo is not in a namespace + for _, rc := range result.ResolvedCalls { + if rc.CallerQN != "" && rc.CalleeQN != "" { + if strings.Contains(rc.CalleeQN, "unknown_func") && rc.Strategy != "lsp_unresolved" { + t.Errorf("ADL should not resolve unknown_func for non-namespaced type, got strategy=%s", rc.Strategy) + } + } + } +} + +// ============================================================================ +// Task 1: Overload Resolution by Parameter Type +// ============================================================================ + +func TestCLSP_OverloadByType(t *testing.T) { + source := ` +class Widget {}; +class Gadget {}; + +class Foo { +public: + void process(Widget* w) {} + void process(Gadget* g) {} +}; + +void test() { + Foo f; + Widget w; + f.process(&w); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "process") +} + +func TestCLSP_OverloadByTypeMethod(t *testing.T) { + source := ` +class Renderer { +public: + void draw(int x) {} + void draw(double x) {} +}; + +void test() { + Renderer r; + r.draw(42); + r.draw(3.14); +} +` + result := extractCPPWithRegistry(t, source) + calls := findAllResolvedCalls(t, result, "test", "draw") + if len(calls) < 2 { + t.Errorf("expected at least 2 draw calls resolved, got %d", len(calls)) + } +} + +// ============================================================================ +// Task 2: Lambda Return Type Inference +// ============================================================================ + +func TestCLSP_LambdaTrailingReturn(t *testing.T) { + source := ` +class Widget { +public: + void draw() {} +}; + +void test() { + auto fn = [](int x) -> Widget { Widget w; return w; }; + fn(1).draw(); +} +` + result := extractCPPWithRegistry(t, source) + // Lambda has trailing return type -> Widget, so .draw() should resolve + rc := findResolvedCall(t, result, "test", "draw") + if rc == nil { + t.Log("lambda trailing return type not resolving draw — tree-sitter may not expose trailing_return_type on lambda") + } +} + +func TestCLSP_LambdaBodyInference(t *testing.T) { + source := ` +class Widget { +public: + void activate() {} +}; + +Widget global_widget; + +void test() { + auto fn = []() { return global_widget; }; +} +` + result := extractCPPWithRegistry(t, source) + // Just verify no crash and the lambda is parsed + _ = result +} + +// ============================================================================ +// Task 3: Inline Namespace Normalization +// ============================================================================ + +func TestCLSP_InlineNamespace_Libc(t *testing.T) { + source := ` +namespace std { +namespace __1 { +class string { +public: + int size() { return 0; } +}; +} +} + +void test() { + std::__1::string s; + s.size(); +} +` + result := extractCPPWithRegistry(t, source) + // std::__1::string should normalize to std.string + requireResolvedCall(t, result, "test", "size") +} + +func TestCLSP_InlineNamespace_GCC(t *testing.T) { + source := ` +namespace std { +namespace __cxx11 { +class basic_string { +public: + int length() { return 0; } +}; +} +} + +void test() { + std::__cxx11::basic_string s; + s.length(); +} +` + result := extractCPPWithRegistry(t, source) + // std::__cxx11::basic_string should normalize to std.basic_string + requireResolvedCall(t, result, "test", "length") +} + +// ============================================================================ +// Task 4: Implicit Conversions +// ============================================================================ + +func TestCLSP_ImplicitStringConversion(t *testing.T) { + source := ` +namespace std { +class string { +public: + int size() { return 0; } +}; +} + +class Logger { +public: + void log(std::string msg) {} + void log(int code) {} +}; + +void test() { + Logger l; + l.log("hello"); + l.log(42); +} +` + result := extractCPPWithRegistry(t, source) + calls := findAllResolvedCalls(t, result, "test", "log") + if len(calls) < 2 { + t.Errorf("expected at least 2 log calls, got %d", len(calls)) + } +} + +func TestCLSP_NumericPromotion(t *testing.T) { + source := ` +class Math { +public: + double compute(double x) { return x; } + int compute(int x) { return x; } +}; + +void test() { + Math m; + m.compute(42); + m.compute(3.14); +} +` + result := extractCPPWithRegistry(t, source) + calls := findAllResolvedCalls(t, result, "test", "compute") + if len(calls) < 2 { + t.Errorf("expected at least 2 compute calls, got %d", len(calls)) + } +} + +// ============================================================================ +// Task 5: Virtual Dispatch (Override Preference) +// ============================================================================ + +func TestCLSP_VirtualOverride(t *testing.T) { + source := ` +class Base { +public: + virtual void draw() {} +}; + +class Derived : public Base { +public: + void draw() {} +}; + +void test() { + Derived d; + d.draw(); +} +` + result := extractCPPWithRegistry(t, source) + rc := requireResolvedCall(t, result, "test", "draw") + // Should prefer Derived::draw over Base::draw + if rc.Strategy != "lsp_type_dispatch" && rc.Strategy != "lsp_virtual_dispatch" { + t.Logf("virtual dispatch strategy: %s (expected lsp_type_dispatch or lsp_virtual_dispatch)", rc.Strategy) + } +} + +func TestCLSP_BasePointerCall(t *testing.T) { + source := ` +class Base { +public: + virtual void render() {} +}; + +class Derived : public Base { +}; + +void test() { + Derived d; + d.render(); +} +` + result := extractCPPWithRegistry(t, source) + rc := requireResolvedCall(t, result, "test", "render") + // No override in Derived — should resolve to Base::render via base dispatch + if rc.Strategy != "lsp_base_dispatch" { + t.Logf("base dispatch strategy: %s (expected lsp_base_dispatch)", rc.Strategy) + } +} + +// ============================================================================ +// Task 6: CRTP Detection +// ============================================================================ + +func TestCLSP_CRTP_Basic(t *testing.T) { + source := ` +template +class Base { +public: + T& self() { return static_cast(*this); } + void base_method() { + self().impl(); + } +}; + +class Derived : public Base { +public: + void impl() {} +}; +` + result := extractCPPWithRegistry(t, source) + // CRTP: Base should bind T=Derived, so self().impl() should resolve + rc := findResolvedCall(t, result, "base_method", "impl") + if rc == nil { + t.Log("CRTP resolution not fully working — T not bound to Derived in template scope") + } +} + +func TestCLSP_CRTP_MultiParam(t *testing.T) { + source := ` +template +class CRTPBase { +public: + void apply() { + static_cast(this)->do_work(); + } +}; + +class MyClass : public CRTPBase { +public: + void do_work() {} +}; +` + result := extractCPPWithRegistry(t, source) + // Multi-param CRTP: only T should be bound to MyClass + rc := findResolvedCall(t, result, "apply", "do_work") + if rc == nil { + t.Log("multi-param CRTP not resolving do_work — expected T bound to MyClass") + } +} + +// ============================================================================ +// Task 7: Range-For Iterator Protocol +// ============================================================================ + +func TestCLSP_RangeForMap(t *testing.T) { + source := ` +namespace std { +template +class map { +public: + K* begin() { return nullptr; } +}; + +template +class pair { +public: + A first; + B second; +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::map m; + for (auto& p : m) { + p.second.draw(); + } +} +` + result := extractCPPWithRegistry(t, source) + // Map elements should be pair, so p.second.draw() resolves + rc := findResolvedCall(t, result, "test", "draw") + if rc == nil { + t.Log("map range-for not resolving draw on pair.second — field lookup on std.pair not working") + } +} + +func TestCLSP_RangeForCustomIterator(t *testing.T) { + source := ` +class Widget { +public: + void activate() {} +}; + +class Iterator { +public: + Widget operator*() { Widget w; return w; } +}; + +class Container { +public: + Iterator begin() { Iterator it; return it; } + Iterator end() { Iterator it; return it; } +}; + +void test() { + Container c; + for (auto& w : c) { + w.activate(); + } +} +` + result := extractCPPWithRegistry(t, source) + // Iterator protocol: begin() -> Iterator, operator*() -> Widget + rc := findResolvedCall(t, result, "test", "activate") + if rc == nil { + t.Log("custom iterator protocol not resolving — begin()/operator*() chain not working") + } +} + +// ============================================================================ +// Template Argument Deduction (TAD) +// ============================================================================ + +func TestCLSP_TAD_FreeFunctionIdentity(t *testing.T) { + // Template free function where return type = param type (identity) + source := ` +class Widget { +public: + void draw() {} +}; + +template +T identity(T x) { return x; } + +void test() { + Widget w; + identity(w).draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_TAD_MakePairLike(t *testing.T) { + // Template free function returning pair + source := ` +namespace std { +template +class pair { +public: + A first; + B second; +}; +} + +class Widget { +public: + void activate() {} +}; + +template +std::pair make_my_pair(T a, U b) { std::pair p; return p; } + +void test() { + Widget w; + auto p = make_my_pair(42, w); +} +` + result := extractCPPWithRegistry(t, source) + // Should deduce T=int, U=Widget + _ = result +} + +// ============================================================================ +// Structured Bindings +// ============================================================================ + +func TestCLSP_StructuredBindingPair(t *testing.T) { + source := ` +namespace std { +template +class pair { +public: + A first; + B second; +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::pair p; + auto [x, w] = p; + w.draw(); +} +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "test", "draw") + if rc == nil { + t.Log("structured binding not decomposing pair — w.draw() unresolved") + } +} + +func TestCLSP_StructuredBindingStruct(t *testing.T) { + source := ` +class Engine { +public: + void start() {} +}; + +struct Car { + int year; + Engine engine; +}; + +void test() { + Car c; + auto [y, eng] = c; + eng.start(); +} +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "test", "start") + if rc == nil { + t.Log("structured binding not decomposing struct fields — eng.start() unresolved") + } +} + +// ============================================================================ +// Conditional / Ternary expression type +// ============================================================================ + +func TestCLSP_TernaryType(t *testing.T) { + source := ` +class Widget { +public: + void draw() {} +}; + +Widget global_w; + +void test() { + Widget* p = &global_w; + auto& w = true ? *p : global_w; + w.draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +// ============================================================================ +// Static cast expression type +// ============================================================================ + +// ============================================================================ +// Gap assessment: patterns that should work +// ============================================================================ + +func TestCLSP_ChainedMethodCalls(t *testing.T) { + // builder.setX().setY().build() — common pattern + source := ` +class Widget { +public: + void render() {} +}; + +class Builder { +public: + Builder& setWidth(int w) { return *this; } + Builder& setHeight(int h) { return *this; } + Widget build() { Widget w; return w; } +}; + +void test() { + Builder b; + b.setWidth(10).setHeight(20).build().render(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "render") +} + +func TestCLSP_StdVectorPushBack(t *testing.T) { + // Common pattern: vec.push_back() then vec[i].method() + source := ` +namespace std { +template +class vector { +public: + void push_back(T x) {} + T& operator[](int i) { return *(T*)0; } + int size() { return 0; } +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::vector widgets; + widgets.push_back(Widget()); + widgets[0].draw(); + widgets.size(); +} +` + result := extractCPPWithRegistry(t, source) + for _, rc := range result.ResolvedCalls { + t.Logf(" %s -> %s [%s]", rc.CallerQN, rc.CalleeQN, rc.Strategy) + } + requireResolvedCall(t, result, "test", "push_back") + requireResolvedCall(t, result, "test", "size") + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_IteratorDeref(t *testing.T) { + // it->method() with smart pointer or iterator + source := ` +namespace std { +template +class unique_ptr { +public: + T* operator->() { return (T*)0; } + T& operator*() { return *(T*)0; } +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::unique_ptr p; + p->draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_EnumClassUsage(t *testing.T) { + // enum class shouldn't cause issues + source := ` +class Logger { +public: + void log(int level) {} +}; + +void test() { + Logger l; + l.log(0); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "log") +} + +func TestCLSP_MultipleReturnPaths(t *testing.T) { + // Function returning different types based on branches — should get first return + source := ` +class Widget { +public: + void draw() {} +}; + +Widget make_widget() { Widget w; return w; } + +void test() { + auto w = make_widget(); + w.draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_NestedTemplate(t *testing.T) { + // vector> — nested templates + source := ` +namespace std { +template +class vector { +public: + T& operator[](int i) { return *(T*)0; } +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::vector> grid; + grid[0][0].draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_ConstRef(t *testing.T) { + // const Widget& should resolve methods + source := ` +class Widget { +public: + void draw() {} +}; + +void process(const Widget& w) { + w.draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "process", "draw") +} + +func TestCLSP_StdFunctionCallback(t *testing.T) { + // std::function — functor call + source := ` +namespace std { +template +class function {}; + +template +class function { +public: + R operator()(Args... args) { return R(); } +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::function factory; + factory().draw(); +} +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "test", "draw") + if rc == nil { + t.Log("std::function operator() not resolving — variadic template specialization not supported") + } +} + +func TestCLSP_OptionalValueAccess(t *testing.T) { + // optional::value() / operator*() + source := ` +namespace std { +template +class optional { +public: + T& value() { return *(T*)0; } + T& operator*() { return *(T*)0; } +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::optional opt; + opt.value().draw(); + (*opt).draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_TypedefChain(t *testing.T) { + // using + typedef chains + source := ` +class Widget { +public: + void draw() {} +}; + +using WidgetRef = Widget&; +typedef Widget* WidgetPtr; + +void test(WidgetPtr p) { + p->draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_IfInitStatement(t *testing.T) { + // C++17 if-init: if (auto x = expr; condition) + source := ` +class Widget { +public: + void draw() {} + bool valid() { return true; } +}; + +Widget make() { Widget w; return w; } + +void test() { + if (auto w = make(); w.valid()) { + w.draw(); + } +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") + requireResolvedCall(t, result, "test", "valid") +} + +func TestCLSP_DependentTypeMember(t *testing.T) { + // T::value_type — dependent type member access + source := ` +namespace std { +template +class vector { +public: + T& operator[](int i) { return *(T*)0; } +}; +} + +class Widget { +public: + void draw() {} +}; + +template +void process(Container& c) { + c[0].draw(); +} +` + result := extractCPPWithRegistry(t, source) + // Template function with unknown Container — c[0] returns unknown + rc := findResolvedCall(t, result, "process", "draw") + if rc != nil { + t.Log("dependent type member access resolved — better than expected!") + } else { + t.Log("dependent type: process->draw not resolved (expected — Container is uninstantiated)") + } +} + +func TestCLSP_AutoReturnFunction(t *testing.T) { + // auto return type deduction + source := ` +class Widget { +public: + void draw() {} +}; + +auto make_widget() { + Widget w; + return w; +} + +void test() { + auto w = make_widget(); + w.draw(); +} +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "test", "draw") + if rc != nil { + t.Log("auto return type deduction working!") + } else { + t.Log("auto return: test->draw not resolved (make_widget() returns unknown)") + } +} + +func TestCLSP_MoveSemantics(t *testing.T) { + // std::move should preserve type + source := ` +class Widget { +public: + void draw() {} +}; + +namespace std { +template +T&& move(T& x) { return (T&&)x; } +} + +void test() { + Widget w; + auto w2 = std::move(w); + w2.draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_MultiLevelInheritance(t *testing.T) { + // A -> B -> C, C calls method from A + source := ` +class A { +public: + void base_op() {} +}; + +class B : public A { +public: + void mid_op() {} +}; + +class C : public B { +public: + void leaf_op() {} +}; + +void test() { + C c; + c.base_op(); + c.mid_op(); + c.leaf_op(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "leaf_op") + rc := findResolvedCall(t, result, "test", "base_op") + if rc == nil { + t.Log("multi-level inheritance: base_op not resolved through C->B->A chain") + } +} + +func TestCLSP_RangeForStructuredBinding(t *testing.T) { + // for (auto& [key, val] : map) — combination of range-for + structured binding + source := ` +namespace std { +template +class map { +public: + void* begin() { return 0; } +}; + +template +class pair { +public: + A first; + B second; +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::map m; + for (auto& [key, widget] : m) { + widget.draw(); + } +} +` + result := extractCPPWithRegistry(t, source) + rc := findResolvedCall(t, result, "test", "draw") + if rc != nil { + t.Log("range-for + structured binding resolving draw!") + } else { + t.Log("range-for + structured binding: draw not resolved (structured binding in range-for)") + } +} + +func TestCLSP_CrossFileInclude(t *testing.T) { + // In cross-file mode, functions from other files should resolve + // This tests the basic cross-file infrastructure + source := ` +class Widget { +public: + void draw() {} +}; + +void render(Widget& w) { + w.draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "render", "draw") +} + +func TestCLSP_FunctionReturningRef(t *testing.T) { + // Method returning T& — unwrap ref to get underlying type + source := ` +class Widget { +public: + void draw() {} +}; + +class Container { +public: + Widget& front() { return *(Widget*)0; } +}; + +void test() { + Container c; + c.front().draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_TemplateMethodChain(t *testing.T) { + // vector.front().draw() — uses stdlib-registered std::vector methods + source := ` +namespace std { +template class vector {}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::vector v; + v.front().draw(); +} +` + result := extractCPPWithRegistry(t, source) + for _, rc := range result.ResolvedCalls { + t.Logf(" %s -> %s [%s %.2f]", rc.CallerQN, rc.CalleeQN, rc.Strategy, rc.Confidence) + } + rc := findResolvedCall(t, result, "test", "draw") + if rc != nil && rc.Strategy != "lsp_unresolved" { + t.Logf("template method chain resolving! strategy=%s", rc.Strategy) + } else { + t.Log("template method chain: front().draw() not fully resolved") + } +} + +func TestCLSP_AlgorithmWithLambda(t *testing.T) { + // std::for_each(v.begin(), v.end(), [](Widget& w) { w.draw(); }) + source := ` +namespace std { +template +void for_each(It first, It last, Fn f) {} + +template +class vector { +public: + T* begin() { return (T*)0; } + T* end() { return (T*)0; } +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::vector widgets; + std::for_each(widgets.begin(), widgets.end(), [](Widget& w) { + w.draw(); + }); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_StaticCastChain(t *testing.T) { + source := ` +class Base { +public: + void base_method() {} +}; + +class Derived : public Base { +public: + void derived_method() {} +}; + +void test() { + Base* b = nullptr; + static_cast(b)->derived_method(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "derived_method") +} + +// ============================================================================ +// Gap assessment: patterns needed for LSP parity +// ============================================================================ + +func TestCLSP_SmartPointerArrow(t *testing.T) { + source := ` +namespace std { +template class unique_ptr { +public: + T* operator->() { return (T*)0; } + T& operator*() { return *(T*)0; } +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::unique_ptr p; + p->draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_StaticMethodCall(t *testing.T) { + source := ` +class Widget { +public: + static Widget create() { return Widget(); } + void draw() {} +}; + +void test() { + Widget w = Widget::create(); + w.draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "create") + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_SubscriptDraw(t *testing.T) { + source := ` +namespace std { +template class vector { +public: + T& operator[](int i) { return *(T*)0; } + int size() { return 0; } +}; +} + +class Widget { +public: + void draw() {} +}; + +void test() { + std::vector v; + v[0].draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_AutoFromMethodReturn(t *testing.T) { + source := ` +class Product { +public: + void use() {} +}; + +class Factory { +public: + Product create() { return Product(); } +}; + +void test() { + Factory f; + auto p = f.create(); + p.use(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "create") + requireResolvedCall(t, result, "test", "use") +} + +func TestCLSP_NestedClassReturnType(t *testing.T) { + source := ` +class Factory { +public: + class Product { + public: + void use() {} + }; + Product create() { return Product(); } +}; + +void test() { + Factory f; + auto p = f.create(); + p.use(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "create") + requireResolvedCall(t, result, "test", "use") +} + +func TestCLSP_MakeSharedChain(t *testing.T) { + source := ` +namespace std { +template class shared_ptr { +public: + T* operator->() { return (T*)0; } +}; +template shared_ptr make_shared() { return shared_ptr(); } +} + +class Widget { +public: + void draw() {} +}; + +void test() { + auto p = std::make_shared(); + p->draw(); +} +` + result := extractCPPWithRegistry(t, source) + requireResolvedCall(t, result, "test", "make_shared") + requireResolvedCall(t, result, "test", "draw") +} + +func TestCLSP_DependentMemberCall(t *testing.T) { + source := ` +class Widget { +public: + void draw() {} +}; + +template +void process(T item) { + item.draw(); +} + +void test() { + Widget w; + process(w); +} +` + result := extractCPPWithRegistry(t, source) + // process(w) should be resolved + requireResolvedCall(t, result, "test", "process") + // item.draw() inside process should be resolved via template instantiation + found := false + for _, rc := range result.ResolvedCalls { + if strings.Contains(rc.CalleeQN, "Widget") && strings.Contains(rc.CalleeQN, "draw") && + rc.Strategy != "lsp_unresolved" { + found = true + t.Logf("dependent member call resolved: %s [%s]", rc.CalleeQN, rc.Strategy) + } + } + if !found { + t.Error("dependent member call item.draw() not resolved via template instantiation") + for _, rc := range result.ResolvedCalls { + t.Logf(" %s -> %s [%s]", rc.CallerQN, rc.CalleeQN, rc.Strategy) + } + } +} diff --git a/internal/cbm/preprocessor.cpp b/internal/cbm/preprocessor.cpp new file mode 100644 index 000000000..69f91c35a --- /dev/null +++ b/internal/cbm/preprocessor.cpp @@ -0,0 +1,95 @@ +// Unity build: include simplecpp implementation directly since CGo only +// compiles .cpp files from the immediate package directory, not subdirs. +#include "vendored/simplecpp/simplecpp.cpp" + +#include "preprocessor.h" +#include "vendored/simplecpp/simplecpp.h" + +#include +#include +#include +#include +#include + +extern "C" { + +char* cbm_preprocess( + const char* source, int source_len, + const char* filename, + const char** extra_defines, + const char** include_paths, + int cpp_mode +) { + if (!source || source_len <= 0) return NULL; + + // Fast-path: skip if no preprocessor directives worth expanding. + bool has_macros = false; + for (int i = 0; i < source_len - 1; i++) { + if (source[i] == '#') { + // Skip whitespace after # + int j = i + 1; + while (j < source_len && (source[j] == ' ' || source[j] == '\t')) j++; + int remaining = source_len - j; + if (remaining >= 6 && strncmp(source + j, "define", 6) == 0) { + has_macros = true; + break; + } + if (remaining >= 5 && strncmp(source + j, "ifdef", 5) == 0) { + has_macros = true; + break; + } + if (remaining >= 6 && strncmp(source + j, "ifndef", 6) == 0) { + has_macros = true; + break; + } + if (remaining >= 3 && strncmp(source + j, "if ", 3) == 0) { + has_macros = true; + break; + } + } + } + if (!has_macros) return NULL; // NULL = no expansion needed, use original + + try { + simplecpp::DUI dui; + if (extra_defines) { + for (int i = 0; extra_defines[i]; i++) + dui.defines.push_back(extra_defines[i]); + } + if (include_paths) { + for (int i = 0; include_paths[i]; i++) + dui.includePaths.push_back(include_paths[i]); + } + dui.std = cpp_mode ? "c++17" : "c11"; + + std::string src(source, source_len); + std::istringstream istr(src); + std::vector files; + files.push_back(filename ? filename : ""); + + simplecpp::TokenList rawtokens(istr, files, files[0]); + simplecpp::TokenList output(files); + simplecpp::FileDataCache filedata = simplecpp::load(rawtokens, files, dui); + + simplecpp::preprocess(output, rawtokens, files, filedata, dui); + + std::string result = output.stringify(); + + // Clean up loaded file data + simplecpp::cleanup(filedata); + + char* out = (char*)malloc(result.size() + 1); + if (!out) return NULL; + memcpy(out, result.c_str(), result.size() + 1); + return out; + } catch (...) { + // Graceful fallback: return NULL = use original source + return NULL; + } +} + +void cbm_preprocess_free(char* expanded) { + free(expanded); +} + +} // extern "C" diff --git a/internal/cbm/preprocessor.h b/internal/cbm/preprocessor.h new file mode 100644 index 000000000..a34dfff84 --- /dev/null +++ b/internal/cbm/preprocessor.h @@ -0,0 +1,30 @@ +#ifndef CBM_PREPROCESSOR_H +#define CBM_PREPROCESSOR_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Preprocess C/C++ source: expand macros, evaluate #ifdef, resolve #include. +// Returns malloc-allocated expanded source, or NULL if no expansion needed/on failure. +// extra_defines: NULL-terminated array of "NAME=VALUE" strings (can be NULL). +// include_paths: NULL-terminated array of directory paths for #include resolution (can be NULL). +// The returned string must be freed with cbm_preprocess_free(). +char* cbm_preprocess( + const char* source, int source_len, + const char* filename, + const char** extra_defines, + const char** include_paths, + int cpp_mode +); + +// Free preprocessed source returned by cbm_preprocess. +void cbm_preprocess_free(char* expanded); + +#ifdef __cplusplus +} +#endif + +#endif // CBM_PREPROCESSOR_H diff --git a/internal/cbm/vendored/simplecpp/simplecpp.cpp b/internal/cbm/vendored/simplecpp/simplecpp.cpp new file mode 100644 index 000000000..7afc17ab7 --- /dev/null +++ b/internal/cbm/vendored/simplecpp/simplecpp.cpp @@ -0,0 +1,3953 @@ +/* + * simplecpp - A simple and high-fidelity C/C++ preprocessor library + * Copyright (C) 2016-2023 simplecpp team + */ + +#if defined(_WIN32) +# ifndef _WIN32_WINNT +# define _WIN32_WINNT 0x0602 +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include +# undef ERROR +#endif + +#include "simplecpp.h" + +#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__) +# define SIMPLECPP_WINDOWS +#endif + +#include +#include +#include +#include +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef SIMPLECPP_WINDOWS +# include +#endif +#include +#include +#include + +#ifdef _WIN32 +# include +#else +# include +#endif + +static bool isHex(const std::string &s) +{ + return s.size()>2 && (s.compare(0,2,"0x")==0 || s.compare(0,2,"0X")==0); +} + +static bool isOct(const std::string &s) +{ + return s.size()>1 && (s[0]=='0') && (s[1] >= '0') && (s[1] < '8'); +} + +static bool isStringLiteral(const std::string &s) +{ + return s.size() > 1 && (s[0]=='\"') && (*s.rbegin()=='\"'); +} + +static bool isCharLiteral(const std::string &s) +{ + // char literal patterns can include 'a', '\t', '\000', '\xff', 'abcd', and maybe '' + // This only checks for the surrounding '' but doesn't parse the content. + return s.size() > 1 && (s[0]=='\'') && (*s.rbegin()=='\''); +} + +static const simplecpp::TokenString DEFINE("define"); +static const simplecpp::TokenString UNDEF("undef"); + +static const simplecpp::TokenString INCLUDE("include"); + +static const simplecpp::TokenString ERROR("error"); +static const simplecpp::TokenString WARNING("warning"); + +static const simplecpp::TokenString IF("if"); +static const simplecpp::TokenString IFDEF("ifdef"); +static const simplecpp::TokenString IFNDEF("ifndef"); +static const simplecpp::TokenString DEFINED("defined"); +static const simplecpp::TokenString ELSE("else"); +static const simplecpp::TokenString ELIF("elif"); +static const simplecpp::TokenString ENDIF("endif"); + +static const simplecpp::TokenString PRAGMA("pragma"); +static const simplecpp::TokenString ONCE("once"); + +static const simplecpp::TokenString HAS_INCLUDE("__has_include"); + +template static std::string toString(T t) +{ + // NOLINTNEXTLINE(misc-const-correctness) - false positive + std::ostringstream ostr; + ostr << t; + return ostr.str(); +} + +#ifdef SIMPLECPP_DEBUG_MACRO_EXPANSION +static std::string locstring(const simplecpp::Location &loc) +{ + std::ostringstream ostr; + ostr << '[' << loc.file() << ':' << loc.line << ':' << loc.col << ']'; + return ostr.str(); +} +#endif + +static long long stringToLL(const std::string &s) +{ + long long ret; + const bool hex = isHex(s); + const bool oct = isOct(s); + std::istringstream istr(hex ? s.substr(2) : oct ? s.substr(1) : s); + if (hex) + istr >> std::hex; + else if (oct) + istr >> std::oct; + istr >> ret; + return ret; +} + +static unsigned long long stringToULL(const std::string &s) +{ + unsigned long long ret; + const bool hex = isHex(s); + const bool oct = isOct(s); + std::istringstream istr(hex ? s.substr(2) : oct ? s.substr(1) : s); + if (hex) + istr >> std::hex; + else if (oct) + istr >> std::oct; + istr >> ret; + return ret; +} + +static bool endsWith(const std::string &s, const std::string &e) +{ + return (s.size() >= e.size()) && std::equal(e.rbegin(), e.rend(), s.rbegin()); +} + +static bool sameline(const simplecpp::Token *tok1, const simplecpp::Token *tok2) +{ + return tok1 && tok2 && tok1->location.sameline(tok2->location); +} + +static bool isAlternativeBinaryOp(const simplecpp::Token *tok, const std::string &alt) +{ + return (tok->name && + tok->str() == alt && + tok->previous && + tok->next && + (tok->previous->number || tok->previous->name || tok->previous->op == ')') && + (tok->next->number || tok->next->name || tok->next->op == '(')); +} + +static bool isAlternativeUnaryOp(const simplecpp::Token *tok, const std::string &alt) +{ + return ((tok->name && tok->str() == alt) && + (!tok->previous || tok->previous->op == '(') && + (tok->next && (tok->next->name || tok->next->number))); +} + +static std::string replaceAll(std::string s, const std::string& from, const std::string& to) +{ + for (size_t pos = s.find(from); pos != std::string::npos; pos = s.find(from, pos + to.size())) + s.replace(pos, from.size(), to); + return s; +} + +void simplecpp::Location::adjust(const std::string &str) +{ + if (strpbrk(str.c_str(), "\r\n") == nullptr) { + col += str.size(); + return; + } + + for (std::size_t i = 0U; i < str.size(); ++i) { + col++; + if (str[i] == '\n' || str[i] == '\r') { + col = 1; + line++; + if (str[i] == '\r' && (i+1)previous) + tok = tok->previous; + for (; tok; tok = tok->next) { + if (tok->previous) { + std::cout << (sameline(tok, tok->previous) ? ' ' : '\n'); + } + std::cout << tok->str(); + } + std::cout << std::endl; +} + +void simplecpp::Token::printOut() const +{ + for (const Token *tok = this; tok; tok = tok->next) { + if (tok != this) { + std::cout << (sameline(tok, tok->previous) ? ' ' : '\n'); + } + std::cout << tok->str(); + } + std::cout << std::endl; +} + +// cppcheck-suppress noConstructor - we call init() in the inherited to initialize the private members +class simplecpp::TokenList::Stream { +public: + virtual ~Stream() = default; + + virtual int get() = 0; + virtual int peek() = 0; + virtual void unget() = 0; + virtual bool good() = 0; + + unsigned char readChar() { + auto ch = static_cast(get()); + + // For UTF-16 encoded files the BOM is 0xfeff/0xfffe. If the + // character is non-ASCII character then replace it with 0xff + if (isUtf16) { + const auto ch2 = static_cast(get()); + const int ch16 = makeUtf16Char(ch, ch2); + ch = static_cast(((ch16 >= 0x80) ? 0xff : ch16)); + } + + // Handling of newlines.. + if (ch == '\r') { + ch = '\n'; + + int ch2 = get(); + if (isUtf16) { + const int c2 = get(); + ch2 = makeUtf16Char(ch2, c2); + } + + if (ch2 != '\n') + ungetChar(); + } + + return ch; + } + + unsigned char peekChar() { + auto ch = static_cast(peek()); + + // For UTF-16 encoded files the BOM is 0xfeff/0xfffe. If the + // character is non-ASCII character then replace it with 0xff + if (isUtf16) { + (void)get(); + const auto ch2 = static_cast(peek()); + unget(); + const int ch16 = makeUtf16Char(ch, ch2); + ch = static_cast(((ch16 >= 0x80) ? 0xff : ch16)); + } + + // Handling of newlines.. + if (ch == '\r') + ch = '\n'; + + return ch; + } + + void ungetChar() { + unget(); + if (isUtf16) + unget(); + } + +protected: + void init() { + // initialize since we use peek() in getAndSkipBOM() + isUtf16 = false; + bom = getAndSkipBOM(); + isUtf16 = (bom == 0xfeff || bom == 0xfffe); + } + +private: + inline int makeUtf16Char(const unsigned char ch, const unsigned char ch2) const { + return (bom == 0xfeff) ? (ch<<8 | ch2) : (ch2<<8 | ch); + } + + unsigned short getAndSkipBOM() { + const int ch1 = peek(); + + // The UTF-16 BOM is 0xfffe or 0xfeff. + if (ch1 >= 0xfe) { + (void)get(); + const unsigned short byte = (static_cast(ch1) << 8); + if (peek() >= 0xfe) + return byte | static_cast(get()); + unget(); + return 0; + } + + // Skip UTF-8 BOM 0xefbbbf + if (ch1 == 0xef) { + (void)get(); + if (peek() == 0xbb) { + (void)get(); + if (peek() == 0xbf) { + (void)get(); + return 0; + } + unget(); + } + unget(); + } + + return 0; + } + + unsigned short bom; +protected: + bool isUtf16; +}; + +namespace { + class StdIStream : public simplecpp::TokenList::Stream { + public: + // cppcheck-suppress uninitDerivedMemberVar - we call Stream::init() to initialize the private members + explicit StdIStream(std::istream &istr) + : istr(istr) { + assert(istr.good()); + init(); + } + + int get() override { + return istr.get(); + } + int peek() override { + return istr.peek(); + } + void unget() override { + istr.unget(); + } + bool good() override { + return istr.good(); + } + + private: + std::istream &istr; + }; + + class StdCharBufStream : public simplecpp::TokenList::Stream { + public: + // cppcheck-suppress uninitDerivedMemberVar - we call Stream::init() to initialize the private members + StdCharBufStream(const unsigned char* str, std::size_t size) + : str(str) + , size(size) + { + init(); + } + + int get() override { + if (pos >= size) + return lastStatus = EOF; + return str[pos++]; + } + int peek() override { + if (pos >= size) + return lastStatus = EOF; + return str[pos]; + } + void unget() override { + --pos; + } + bool good() override { + return lastStatus != EOF; + } + + private: + const unsigned char *str; + const std::size_t size; + std::size_t pos{}; + int lastStatus{}; + }; + + class FileStream : public simplecpp::TokenList::Stream { + public: + /** + * @throws simplecpp::Output thrown if file is not found + */ + // cppcheck-suppress uninitDerivedMemberVar - we call Stream::init() to initialize the private members + explicit FileStream(const std::string &filename, std::vector &files) + : file(fopen(filename.c_str(), "rb")) + { + if (!file) { + files.emplace_back(filename); + throw simplecpp::Output(simplecpp::Output::FILE_NOT_FOUND, {}, "File is missing: " + filename); + } + init(); + } + + FileStream(const FileStream&) = delete; + FileStream &operator=(const FileStream&) = delete; + + ~FileStream() override { + fclose(file); + file = nullptr; + } + + int get() override { + lastStatus = lastCh = fgetc(file); + return lastCh; + } + int peek() override { + // keep lastCh intact + const int ch = fgetc(file); + unget_internal(ch); + return ch; + } + void unget() override { + unget_internal(lastCh); + } + bool good() override { + return lastStatus != EOF; + } + + private: + void unget_internal(int ch) { + if (isUtf16) { + // TODO: use ungetc() as well + // UTF-16 has subsequent unget() calls + fseek(file, -1, SEEK_CUR); + } else { + ungetc(ch, file); + } + } + + FILE *file; + int lastCh{}; + int lastStatus{}; + }; +} + +simplecpp::TokenList::TokenList(std::vector &filenames) : frontToken(nullptr), backToken(nullptr), files(filenames) {} + +simplecpp::TokenList::TokenList(std::istream &istr, std::vector &filenames, const std::string &filename, OutputList *outputList) + : frontToken(nullptr), backToken(nullptr), files(filenames) +{ + StdIStream stream(istr); + readfile(stream,filename,outputList); +} + +simplecpp::TokenList::TokenList(const unsigned char* data, std::size_t size, std::vector &filenames, const std::string &filename, OutputList *outputList, int /*unused*/) + : frontToken(nullptr), backToken(nullptr), files(filenames) +{ + StdCharBufStream stream(data, size); + readfile(stream,filename,outputList); +} + +simplecpp::TokenList::TokenList(const std::string &filename, std::vector &filenames, OutputList *outputList) + : frontToken(nullptr), backToken(nullptr), files(filenames) +{ + try { + FileStream stream(filename, filenames); + readfile(stream,filename,outputList); + } catch (const simplecpp::Output & e) { + outputList->emplace_back(e); + } +} + +simplecpp::TokenList::TokenList(const TokenList &other) : frontToken(nullptr), backToken(nullptr), files(other.files) +{ + *this = other; +} + +simplecpp::TokenList::TokenList(TokenList &&other) : frontToken(nullptr), backToken(nullptr), files(other.files) +{ + *this = std::move(other); +} + +simplecpp::TokenList::~TokenList() +{ + clear(); +} + +simplecpp::TokenList &simplecpp::TokenList::operator=(const TokenList &other) +{ + if (this != &other) { + clear(); + files = other.files; + for (const Token *tok = other.cfront(); tok; tok = tok->next) + push_back(new Token(*tok)); + sizeOfType = other.sizeOfType; + } + return *this; +} + +simplecpp::TokenList &simplecpp::TokenList::operator=(TokenList &&other) +{ + if (this != &other) { + clear(); + frontToken = other.frontToken; + other.frontToken = nullptr; + backToken = other.backToken; + other.backToken = nullptr; + files = other.files; + sizeOfType = std::move(other.sizeOfType); + } + return *this; +} + +void simplecpp::TokenList::clear() +{ + backToken = nullptr; + while (frontToken) { + Token * const next = frontToken->next; + delete frontToken; + frontToken = next; + } + sizeOfType.clear(); +} + +void simplecpp::TokenList::push_back(Token *tok) +{ + if (!frontToken) + frontToken = tok; + else + backToken->next = tok; + tok->previous = backToken; + backToken = tok; +} + +void simplecpp::TokenList::dump(bool linenrs) const +{ + std::cout << stringify(linenrs) << std::endl; +} + +std::string simplecpp::TokenList::stringify(bool linenrs) const +{ + std::ostringstream ret; + Location loc; + loc.line = 1; + bool filechg = true; + for (const Token *tok = cfront(); tok; tok = tok->next) { + if (tok->location.line < loc.line || tok->location.fileIndex != loc.fileIndex) { + ret << "\n#line " << tok->location.line << " \"" << file(tok->location) << "\"\n"; + loc = tok->location; + filechg = true; + } + + if (linenrs && filechg) { + ret << loc.line << ": "; + filechg = false; + } + + while (tok->location.line > loc.line) { + ret << '\n'; + loc.line++; + if (linenrs) + ret << loc.line << ": "; + } + + if (sameline(tok->previous, tok)) + ret << ' '; + + ret << tok->str(); + + loc.adjust(tok->str()); + } + + return ret.str(); +} + +static bool isNameChar(unsigned char ch) +{ + return std::isalnum(ch) || ch == '_' || ch == '$'; +} + +static std::string escapeString(const std::string &str) +{ + std::ostringstream ostr; + ostr << '\"'; + for (std::size_t i = 1U; i < str.size() - 1; ++i) { + const char c = str[i]; + if (c == '\\' || c == '\"' || c == '\'') + ostr << '\\'; + ostr << c; + } + ostr << '\"'; + return ostr.str(); +} + +static void portabilityBackslash(simplecpp::OutputList *outputList, const simplecpp::Location &location) +{ + if (!outputList) + return; + simplecpp::Output err{ + simplecpp::Output::PORTABILITY_BACKSLASH, + location, + "Combination 'backslash space newline' is not portable." + }; + outputList->emplace_back(std::move(err)); +} + +static bool isStringLiteralPrefix(const std::string &str) +{ + return str == "u" || str == "U" || str == "L" || str == "u8" || + str == "R" || str == "uR" || str == "UR" || str == "LR" || str == "u8R"; +} + +void simplecpp::TokenList::lineDirective(unsigned int fileIndex, unsigned int line, Location &location) +{ + if (fileIndex != location.fileIndex || line >= location.line) { + location.fileIndex = fileIndex; + location.line = line; + return; + } + + if (line + 2 >= location.line) { + location.line = line; + while (cback()->op != '#') + deleteToken(back()); + deleteToken(back()); + return; + } +} + +static const std::string COMMENT_END("*/"); + +void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, OutputList *outputList) +{ + std::stack loc; + + unsigned int multiline = 0U; + + const Token *oldLastToken = nullptr; + + Location location(fileIndex(filename), 1, 1); + while (stream.good()) { + unsigned char ch = stream.readChar(); + if (!stream.good()) + break; + + if (ch >= 0x80) { + if (outputList) { + simplecpp::Output err{ + simplecpp::Output::UNHANDLED_CHAR_ERROR, + location, + "The code contains unhandled character(s) (character code=" + std::to_string(static_cast(ch)) + "). Neither unicode nor extended ascii is supported." + }; + outputList->emplace_back(std::move(err)); + } + clear(); + return; + } + + if (ch == '\n') { + if (cback() && cback()->op == '\\') { + if (location.col > cback()->location.col + 1U) + portabilityBackslash(outputList, cback()->location); + ++multiline; + deleteToken(back()); + } else { + location.line += multiline + 1; + multiline = 0U; + } + if (!multiline) + location.col = 1; + + if (oldLastToken != cback()) { + oldLastToken = cback(); + const Token * const llTok = isLastLinePreprocessor(); + if (!llTok) + continue; + const Token * const llNextToken = llTok->next; + if (!llTok->next) + continue; + if (llNextToken->next) { + // #file "file.c" + if (llNextToken->str() == "file" && + llNextToken->next->str()[0] == '\"') + { + const Token *strtok = cback(); + while (strtok->comment) + strtok = strtok->previous; + loc.push(location); + location.fileIndex = fileIndex(strtok->str().substr(1U, strtok->str().size() - 2U)); + location.line = 1U; + } + // TODO: add support for "# 3" + // #3 "file.c" + // #line 3 "file.c" + else if ((llNextToken->number && + llNextToken->next->str()[0] == '\"') || + (llNextToken->str() == "line" && + llNextToken->next->number && + llNextToken->next->next && + llNextToken->next->next->str()[0] == '\"')) + { + const Token *strtok = cback(); + while (strtok->comment) + strtok = strtok->previous; + const Token *numtok = strtok->previous; + while (numtok->comment) + numtok = numtok->previous; + lineDirective(fileIndex(replaceAll(strtok->str().substr(1U, strtok->str().size() - 2U),"\\\\","\\")), + std::atol(numtok->str().c_str()), location); + } + // #line 3 + else if (llNextToken->str() == "line" && + llNextToken->next->number) + { + const Token *numtok = cback(); + while (numtok->comment) + numtok = numtok->previous; + lineDirective(location.fileIndex, std::atol(numtok->str().c_str()), location); + } + } + // #endfile + else if (llNextToken->str() == "endfile" && !loc.empty()) + { + location = loc.top(); + loc.pop(); + } + } + + continue; + } + + if (ch <= ' ') { + location.col++; + continue; + } + + TokenString currentToken; + + if (cback() && cback()->location.line == location.line && cback()->previous && cback()->previous->op == '#') { + const Token* const ppTok = cback()->previous; + if (ppTok->next && (ppTok->next->str() == "error" || ppTok->next->str() == "warning")) { + char prev = ' '; + while (stream.good() && (prev == '\\' || (ch != '\r' && ch != '\n'))) { + currentToken += ch; + prev = ch; + ch = stream.readChar(); + } + stream.ungetChar(); + push_back(new Token(currentToken, location)); + location.adjust(currentToken); + continue; + } + } + + // number or name + if (isNameChar(ch)) { + const bool num = !!std::isdigit(ch); + while (stream.good() && isNameChar(ch)) { + currentToken += ch; + ch = stream.readChar(); + if (num && ch=='\'' && isNameChar(stream.peekChar())) + ch = stream.readChar(); + } + + stream.ungetChar(); + } + + // comment + else if (ch == '/' && stream.peekChar() == '/') { + while (stream.good() && ch != '\n') { + currentToken += ch; + ch = stream.readChar(); + if (ch == '\\') { + TokenString tmp; + char tmp_ch = ch; + while ((stream.good()) && (tmp_ch == '\\' || tmp_ch == ' ' || tmp_ch == '\t')) { + tmp += tmp_ch; + tmp_ch = stream.readChar(); + } + if (!stream.good()) { + break; + } + + if (tmp_ch != '\n') { + currentToken += tmp; + } else { + const TokenString check_portability = currentToken + tmp; + const std::string::size_type pos = check_portability.find_last_not_of(" \t"); + if (pos < check_portability.size() - 1U && check_portability[pos] == '\\') + portabilityBackslash(outputList, location); + ++multiline; + tmp_ch = stream.readChar(); + currentToken += '\n'; + } + ch = tmp_ch; + } + } + if (ch == '\n') { + stream.ungetChar(); + } + } + + // comment + else if (ch == '/' && stream.peekChar() == '*') { + currentToken = "/*"; + (void)stream.readChar(); + ch = stream.readChar(); + while (stream.good()) { + currentToken += ch; + if (currentToken.size() >= 4U && endsWith(currentToken, COMMENT_END)) + break; + ch = stream.readChar(); + } + // multiline.. + + std::string::size_type pos = 0; + while ((pos = currentToken.find("\\\n",pos)) != std::string::npos) { + currentToken.erase(pos,2); + ++multiline; + } + if (multiline || isLastLinePreprocessor()) { + pos = 0; + while ((pos = currentToken.find('\n',pos)) != std::string::npos) { + currentToken.erase(pos,1); + ++multiline; + } + } + } + + // string / char literal + else if (ch == '\"' || ch == '\'') { + std::string prefix; + if (cback() && cback()->name && isStringLiteralPrefix(cback()->str()) && + ((cback()->location.col + cback()->str().size()) == location.col) && + (cback()->location.line == location.line)) { + prefix = cback()->str(); + } + // C++11 raw string literal + if (ch == '\"' && !prefix.empty() && *cback()->str().rbegin() == 'R') { + std::string delim; + currentToken = ch; + prefix.resize(prefix.size() - 1); + ch = stream.readChar(); + while (stream.good() && ch != '(' && ch != '\n') { + delim += ch; + ch = stream.readChar(); + } + if (!stream.good() || ch == '\n') { + if (outputList) { + Output err{ + Output::SYNTAX_ERROR, + location, + "Invalid newline in raw string delimiter." + }; + outputList->emplace_back(std::move(err)); + } + return; + } + const std::string endOfRawString(')' + delim + currentToken); + while (stream.good() && (!endsWith(currentToken, endOfRawString) || currentToken.size() <= 1)) + currentToken += stream.readChar(); + if (!endsWith(currentToken, endOfRawString)) { + if (outputList) { + Output err{ + Output::SYNTAX_ERROR, + location, + "Raw string missing terminating delimiter." + }; + outputList->emplace_back(std::move(err)); + } + return; + } + currentToken.erase(currentToken.size() - endOfRawString.size(), endOfRawString.size() - 1U); + currentToken = escapeString(currentToken); + currentToken.insert(0, prefix); + back()->setstr(currentToken); + location.adjust(currentToken); + if (currentToken.find_first_of("\r\n") == std::string::npos) + location.col += 2 + (2 * delim.size()); + else + location.col += 1 + delim.size(); + + continue; + } + + currentToken = readUntil(stream,location,ch,ch,outputList); + if (currentToken.size() < 2U) + // Error is reported by readUntil() + return; + + std::string s = currentToken; + std::string::size_type pos; + int newlines = 0; + while ((pos = s.find_first_of("\r\n")) != std::string::npos) { + s.erase(pos,1); + newlines++; + } + + if (prefix.empty()) + push_back(new Token(s, location, !!std::isspace(stream.peekChar()))); // push string without newlines + else + back()->setstr(prefix + s); + + if (newlines > 0) { + const Token * const llTok = lastLineTok(); + if (llTok && llTok->op == '#' && llTok->next && (llTok->next->str() == "define" || llTok->next->str() == "pragma") && llTok->next->next) { + multiline += newlines; + location.adjust(s); + continue; + } + } + + location.adjust(currentToken); + continue; + } + + else { + currentToken += ch; + } + + if (*currentToken.begin() == '<') { + const Token * const llTok = lastLineTok(); + if (llTok && llTok->op == '#' && llTok->next && llTok->next->str() == "include") { + currentToken = readUntil(stream, location, '<', '>', outputList); + if (currentToken.size() < 2U) + return; + } + } + + push_back(new Token(currentToken, location, !!std::isspace(stream.peekChar()))); + + if (multiline) + location.col += currentToken.size(); + else + location.adjust(currentToken); + } + + combineOperators(); +} + +void simplecpp::TokenList::constFold() +{ + while (cfront()) { + // goto last '(' + Token *tok = back(); + while (tok && tok->op != '(') + tok = tok->previous; + + // no '(', goto first token + if (!tok) + tok = front(); + + // Constant fold expression + constFoldUnaryNotPosNeg(tok); + constFoldMulDivRem(tok); + constFoldAddSub(tok); + constFoldShift(tok); + constFoldComparison(tok); + constFoldBitwise(tok); + constFoldLogicalOp(tok); + constFoldQuestionOp(tok); + + // If there is no '(' we are done with the constant folding + if (tok->op != '(') + break; + + if (!tok->next || !tok->next->next || tok->next->next->op != ')') + break; + + tok = tok->next; + deleteToken(tok->previous); + deleteToken(tok->next); + } +} + +static bool isFloatSuffix(const simplecpp::Token *tok) +{ + if (!tok || tok->str().size() != 1U) + return false; + const char c = std::tolower(tok->str()[0]); + return c == 'f' || c == 'l'; +} + +static const std::string AND("and"); +static const std::string BITAND("bitand"); +static const std::string BITOR("bitor"); +static bool isAlternativeAndBitandBitor(const simplecpp::Token* tok) +{ + return isAlternativeBinaryOp(tok, AND) || isAlternativeBinaryOp(tok, BITAND) || isAlternativeBinaryOp(tok, BITOR); +} + +void simplecpp::TokenList::combineOperators() +{ + std::stack executableScope; + executableScope.push(false); + for (Token *tok = front(); tok; tok = tok->next) { + if (tok->op == '{') { + if (executableScope.top()) { + executableScope.push(true); + continue; + } + const Token *prev = tok->previous; + while (prev && prev->isOneOf(";{}()")) + prev = prev->previous; + executableScope.push(prev && prev->op == ')'); + continue; + } + if (tok->op == '}') { + if (executableScope.size() > 1) + executableScope.pop(); + continue; + } + + if (tok->op == '.') { + // ellipsis ... + if (tok->next && tok->next->op == '.' && tok->next->location.col == (tok->location.col + 1) && + tok->next->next && tok->next->next->op == '.' && tok->next->next->location.col == (tok->location.col + 2)) { + tok->setstr("..."); + deleteToken(tok->next); + deleteToken(tok->next); + continue; + } + // float literals.. + if (tok->previous && tok->previous->number && sameline(tok->previous, tok) && tok->previous->str().find_first_of("._") == std::string::npos) { + tok->setstr(tok->previous->str() + '.'); + deleteToken(tok->previous); + if (sameline(tok, tok->next) && (isFloatSuffix(tok->next) || (tok->next && tok->next->startsWithOneOf("AaBbCcDdEeFfPp") && !isAlternativeAndBitandBitor(tok->next)))) { + tok->setstr(tok->str() + tok->next->str()); + deleteToken(tok->next); + } + } + if (tok->next && tok->next->number) { + tok->setstr(tok->str() + tok->next->str()); + deleteToken(tok->next); + } + } + // match: [0-9.]+E [+-] [0-9]+ + const char lastChar = tok->str()[tok->str().size() - 1]; + if (tok->number && !isOct(tok->str()) && + ((!isHex(tok->str()) && (lastChar == 'E' || lastChar == 'e')) || + (isHex(tok->str()) && (lastChar == 'P' || lastChar == 'p'))) && + tok->next && tok->next->isOneOf("+-") && tok->next->next && tok->next->next->number) { + tok->setstr(tok->str() + tok->next->op + tok->next->next->str()); + deleteToken(tok->next); + deleteToken(tok->next); + } + + if (tok->op == '\0' || !tok->next || tok->next->op == '\0') + continue; + if (!sameline(tok,tok->next)) + continue; + if (tok->location.col + 1U != tok->next->location.col) + continue; + + if (tok->next->op == '=' && tok->isOneOf("=!<>+-*/%&|^")) { + if (tok->op == '&' && !executableScope.top()) { + // don't combine &= if it is a anonymous reference parameter with default value: + // void f(x&=2) + int indentlevel = 0; + const Token *start = tok; + while (indentlevel >= 0 && start) { + if (start->op == ')') + ++indentlevel; + else if (start->op == '(') + --indentlevel; + else if (start->isOneOf(";{}")) + break; + start = start->previous; + } + if (indentlevel == -1 && start) { + const Token * const ftok = start; + bool isFuncDecl = ftok->name; + while (isFuncDecl) { + if (!start->name && start->str() != "::" && start->op != '*' && start->op != '&') + isFuncDecl = false; + if (!start->previous) + break; + if (start->previous->isOneOf(";{}:")) + break; + start = start->previous; + } + isFuncDecl &= start != ftok && start->name; + if (isFuncDecl) { + // TODO: we could loop through the parameters here and check if they are correct. + continue; + } + } + } + tok->setstr(tok->str() + "="); + deleteToken(tok->next); + } else if ((tok->op == '|' || tok->op == '&') && tok->op == tok->next->op) { + tok->setstr(tok->str() + tok->next->str()); + deleteToken(tok->next); + } else if (tok->op == ':' && tok->next->op == ':') { + tok->setstr(tok->str() + tok->next->str()); + deleteToken(tok->next); + } else if (tok->op == '-' && tok->next->op == '>') { + tok->setstr(tok->str() + tok->next->str()); + deleteToken(tok->next); + } else if ((tok->op == '<' || tok->op == '>') && tok->op == tok->next->op) { + tok->setstr(tok->str() + tok->next->str()); + deleteToken(tok->next); + if (tok->next && tok->next->op == '=' && tok->next->next && tok->next->next->op != '=') { + tok->setstr(tok->str() + tok->next->str()); + deleteToken(tok->next); + } + } else if ((tok->op == '+' || tok->op == '-') && tok->op == tok->next->op) { + if (tok->location.col + 1U != tok->next->location.col) + continue; + if (tok->previous && tok->previous->number) + continue; + if (tok->next->next && tok->next->next->number) + continue; + tok->setstr(tok->str() + tok->next->str()); + deleteToken(tok->next); + } + } +} + +static const std::string COMPL("compl"); +static const std::string NOT("not"); +void simplecpp::TokenList::constFoldUnaryNotPosNeg(simplecpp::Token *tok) +{ + for (; tok && tok->op != ')'; tok = tok->next) { + // "not" might be ! + if (isAlternativeUnaryOp(tok, NOT)) + tok->op = '!'; + // "compl" might be ~ + else if (isAlternativeUnaryOp(tok, COMPL)) + tok->op = '~'; + + if (tok->op == '!' && tok->next && tok->next->number) { + tok->setstr(tok->next->str() == "0" ? "1" : "0"); + deleteToken(tok->next); + } else if (tok->op == '~' && tok->next && tok->next->number) { + tok->setstr(toString(~stringToLL(tok->next->str()))); + deleteToken(tok->next); + } else { + if (tok->previous && (tok->previous->number || tok->previous->name)) + continue; + if (!tok->next || !tok->next->number) + continue; + switch (tok->op) { + case '+': + tok->setstr(tok->next->str()); + deleteToken(tok->next); + break; + case '-': + tok->setstr(tok->op + tok->next->str()); + deleteToken(tok->next); + break; + } + } + } +} + +void simplecpp::TokenList::constFoldMulDivRem(Token *tok) +{ + for (; tok && tok->op != ')'; tok = tok->next) { + if (!tok->previous || !tok->previous->number) + continue; + if (!tok->next || !tok->next->number) + continue; + + long long result; + if (tok->op == '*') { + result = (stringToLL(tok->previous->str()) * stringToLL(tok->next->str())); + } + else if (tok->op == '/' || tok->op == '%') { + const long long rhs = stringToLL(tok->next->str()); + if (rhs == 0) + throw std::overflow_error("division/modulo by zero"); + const long long lhs = stringToLL(tok->previous->str()); + if (rhs == -1 && lhs == std::numeric_limits::min()) + throw std::overflow_error("division overflow"); + if (tok->op == '/') + result = (lhs / rhs); + else + result = (lhs % rhs); + } else { + continue; + } + + tok = tok->previous; + tok->setstr(toString(result)); + deleteToken(tok->next); + deleteToken(tok->next); + } +} + +void simplecpp::TokenList::constFoldAddSub(Token *tok) +{ + for (; tok && tok->op != ')'; tok = tok->next) { + if (!tok->previous || !tok->previous->number) + continue; + if (!tok->next || !tok->next->number) + continue; + + long long result; + if (tok->op == '+') + result = stringToLL(tok->previous->str()) + stringToLL(tok->next->str()); + else if (tok->op == '-') + result = stringToLL(tok->previous->str()) - stringToLL(tok->next->str()); + else + continue; + + tok = tok->previous; + tok->setstr(toString(result)); + deleteToken(tok->next); + deleteToken(tok->next); + } +} + +void simplecpp::TokenList::constFoldShift(Token *tok) +{ + for (; tok && tok->op != ')'; tok = tok->next) { + if (!tok->previous || !tok->previous->number) + continue; + if (!tok->next || !tok->next->number) + continue; + + long long result; + if (tok->str() == "<<") + result = stringToLL(tok->previous->str()) << stringToLL(tok->next->str()); + else if (tok->str() == ">>") + result = stringToLL(tok->previous->str()) >> stringToLL(tok->next->str()); + else + continue; + + tok = tok->previous; + tok->setstr(toString(result)); + deleteToken(tok->next); + deleteToken(tok->next); + } +} + +static const std::string NOTEQ("not_eq"); +void simplecpp::TokenList::constFoldComparison(Token *tok) +{ + for (; tok && tok->op != ')'; tok = tok->next) { + if (isAlternativeBinaryOp(tok,NOTEQ)) + tok->setstr("!="); + + if (!tok->startsWithOneOf("<>=!")) + continue; + if (!tok->previous || !tok->previous->number) + continue; + if (!tok->next || !tok->next->number) + continue; + + int result; + if (tok->str() == "==") + result = (stringToLL(tok->previous->str()) == stringToLL(tok->next->str())); + else if (tok->str() == "!=") + result = (stringToLL(tok->previous->str()) != stringToLL(tok->next->str())); + else if (tok->str() == ">") + result = (stringToLL(tok->previous->str()) > stringToLL(tok->next->str())); + else if (tok->str() == ">=") + result = (stringToLL(tok->previous->str()) >= stringToLL(tok->next->str())); + else if (tok->str() == "<") + result = (stringToLL(tok->previous->str()) < stringToLL(tok->next->str())); + else if (tok->str() == "<=") + result = (stringToLL(tok->previous->str()) <= stringToLL(tok->next->str())); + else + continue; + + tok = tok->previous; + tok->setstr(toString(result)); + deleteToken(tok->next); + deleteToken(tok->next); + } +} + +static const std::string XOR("xor"); +void simplecpp::TokenList::constFoldBitwise(Token *tok) +{ + Token * const tok1 = tok; + for (const char *op = "&^|"; *op; op++) { + const std::string* alternativeOp; + if (*op == '&') + alternativeOp = &BITAND; + else if (*op == '|') + alternativeOp = &BITOR; + else + alternativeOp = &XOR; + for (tok = tok1; tok && tok->op != ')'; tok = tok->next) { + if (tok->op != *op && !isAlternativeBinaryOp(tok, *alternativeOp)) + continue; + if (!tok->previous || !tok->previous->number) + continue; + if (!tok->next || !tok->next->number) + continue; + long long result; + if (*op == '&') + result = (stringToLL(tok->previous->str()) & stringToLL(tok->next->str())); + else if (*op == '^') + result = (stringToLL(tok->previous->str()) ^ stringToLL(tok->next->str())); + else /*if (*op == '|')*/ + result = (stringToLL(tok->previous->str()) | stringToLL(tok->next->str())); + tok = tok->previous; + tok->setstr(toString(result)); + deleteToken(tok->next); + deleteToken(tok->next); + } + } +} + +static const std::string OR("or"); +void simplecpp::TokenList::constFoldLogicalOp(Token *tok) +{ + for (; tok && tok->op != ')'; tok = tok->next) { + if (tok->name) { + if (isAlternativeBinaryOp(tok,AND)) + tok->setstr("&&"); + else if (isAlternativeBinaryOp(tok,OR)) + tok->setstr("||"); + } + if (tok->str() != "&&" && tok->str() != "||") + continue; + if (!tok->previous || !tok->previous->number) + continue; + if (!tok->next || !tok->next->number) + continue; + + int result; + if (tok->str() == "||") + result = (stringToLL(tok->previous->str()) || stringToLL(tok->next->str())); + else /*if (tok->str() == "&&")*/ + result = (stringToLL(tok->previous->str()) && stringToLL(tok->next->str())); + + tok = tok->previous; + tok->setstr(toString(result)); + deleteToken(tok->next); + deleteToken(tok->next); + } +} + +void simplecpp::TokenList::constFoldQuestionOp(Token *&tok1) +{ + bool gotoTok1 = false; + // NOLINTNEXTLINE(misc-const-correctness) - technically correct but used to access non-const data + for (Token *tok = tok1; tok && tok->op != ')'; tok = gotoTok1 ? tok1 : tok->next) { + gotoTok1 = false; + if (tok->str() != "?") + continue; + if (!tok->previous || !tok->next || !tok->next->next) + throw std::runtime_error("invalid expression"); + if (!tok->previous->number) + continue; + if (tok->next->next->op != ':') + continue; + Token * const condTok = tok->previous; + Token * const trueTok = tok->next; + Token * const falseTok = trueTok->next->next; + if (!falseTok) + throw std::runtime_error("invalid expression"); + if (condTok == tok1) + tok1 = (condTok->str() != "0" ? trueTok : falseTok); + deleteToken(condTok->next); // ? + deleteToken(trueTok->next); // : + deleteToken(condTok->str() == "0" ? trueTok : falseTok); + deleteToken(condTok); + gotoTok1 = true; + } +} + +void simplecpp::TokenList::removeComments() +{ + Token *tok = frontToken; + while (tok) { + Token * const tok1 = tok; + tok = tok->next; + if (tok1->comment) + deleteToken(tok1); + } +} + +std::string simplecpp::TokenList::readUntil(Stream &stream, const Location &location, const char start, const char end, OutputList *outputList) +{ + std::string ret; + ret += start; + + bool backslash = false; + char ch = 0; + while (ch != end && ch != '\r' && ch != '\n' && stream.good()) { + ch = stream.readChar(); + if (backslash && ch == '\n') { + ch = 0; + backslash = false; + continue; + } + backslash = false; + ret += ch; + if (ch == '\\') { + bool update_ch = false; + char next = 0; + do { + next = stream.readChar(); + if (next == '\r' || next == '\n') { + ret.erase(ret.size()-1U); + backslash = (next == '\r'); + update_ch = false; + } else if (next == '\\') { + update_ch = !update_ch; + } + ret += next; + } while (next == '\\'); + if (update_ch) + ch = next; + } + } + + if (!stream.good() || ch != end) { + clear(); + if (outputList) { + Output err{ + Output::SYNTAX_ERROR, + location, + std::string("No pair for character (") + start + "). Can't process file. File is either invalid or unicode, which is currently not supported." + }; + outputList->emplace_back(std::move(err)); + } + return ""; + } + + return ret; +} + +const simplecpp::Token* simplecpp::TokenList::lastLineTok(int maxsize) const +{ + const Token* prevTok = nullptr; + int count = 0; + for (const Token *tok = cback(); ; tok = tok->previous) { + if (!sameline(tok, cback())) + break; + if (tok->comment) + continue; + if (++count > maxsize) + return nullptr; + prevTok = tok; + } + return prevTok; +} + +const simplecpp::Token* simplecpp::TokenList::isLastLinePreprocessor(int maxsize) const +{ + const Token * const prevTok = lastLineTok(maxsize); + if (prevTok && prevTok->op == '#') + return prevTok; + return nullptr; +} + +unsigned int simplecpp::TokenList::fileIndex(const std::string &filename) +{ + for (unsigned int i = 0; i < files.size(); ++i) { + if (files[i] == filename) + return i; + } + files.emplace_back(filename); + return files.size() - 1U; +} + +const std::string& simplecpp::TokenList::file(const Location& loc) const +{ + static const std::string s_emptyFileName; + return loc.fileIndex < files.size() ? files[loc.fileIndex] : s_emptyFileName; +} + + +namespace simplecpp { + class Macro; + using MacroMap = std::unordered_map; + + class Macro { + public: + explicit Macro(std::vector &f) : nameTokDef(nullptr), valueToken(nullptr), endToken(nullptr), files(f), tokenListDefine(f), variadic(false), variadicOpt(false), valueDefinedInCode_(false) {} + + /** + * @throws std::runtime_error thrown on bad macro syntax + */ + Macro(const Token *tok, std::vector &f) : nameTokDef(nullptr), files(f), tokenListDefine(f), valueDefinedInCode_(true) { + if (sameline(tok->previousSkipComments(), tok)) + throw std::runtime_error("bad macro syntax"); + if (tok->op != '#') + throw std::runtime_error("bad macro syntax"); + const Token * const hashtok = tok; + tok = tok->next; + if (!tok || tok->str() != DEFINE) + throw std::runtime_error("bad macro syntax"); + tok = tok->next; + if (!tok || !tok->name || !sameline(hashtok,tok)) + throw std::runtime_error("bad macro syntax"); + if (!parseDefine(tok)) + throw std::runtime_error("bad macro syntax"); + } + + /** + * @throws std::runtime_error thrown on bad macro syntax + */ + Macro(const std::string &name, const std::string &value, std::vector &f) : nameTokDef(nullptr), files(f), tokenListDefine(f), valueDefinedInCode_(false) { + const std::string def(name + ' ' + value); + StdCharBufStream stream(reinterpret_cast(def.data()), def.size()); + tokenListDefine.readfile(stream); + if (!parseDefine(tokenListDefine.cfront())) + throw std::runtime_error("bad macro syntax. macroname=" + name + " value=" + value); + } + + Macro(const Macro &other) : nameTokDef(nullptr), files(other.files), tokenListDefine(other.files), valueDefinedInCode_(other.valueDefinedInCode_) { + // TODO: remove the try-catch - see #537 + // avoid bugprone-exception-escape clang-tidy warning + try { + *this = other; + } + catch (const Error&) {} // NOLINT(bugprone-empty-catch) + } + + ~Macro() { + delete optExpandValue; + delete optNoExpandValue; + } + + Macro &operator=(const Macro &other) { + if (this != &other) { + files = other.files; + valueDefinedInCode_ = other.valueDefinedInCode_; + if (other.tokenListDefine.empty()) { + parseDefine(other.nameTokDef); + } + else { + tokenListDefine = other.tokenListDefine; + parseDefine(tokenListDefine.cfront()); + } + usageList = other.usageList; + } + return *this; + } + + bool valueDefinedInCode() const { + return valueDefinedInCode_; + } + + /** + * Expand macro. This will recursively expand inner macros. + * @param output destination tokenlist + * @param rawtok macro token + * @param macros list of macros + * @param inputFiles the input files + * @return token after macro + * @throws Error thrown on missing or invalid preprocessor directives + * @throws wrongNumberOfParameters thrown on invalid number of parameters + * @throws invalidHashHash thrown on invalid ## usage + */ + const Token * expand(TokenList & output, + const Token * rawtok, + const MacroMap ¯os, + std::vector &inputFiles) const { + std::set expandedmacros; + +#ifdef SIMPLECPP_DEBUG_MACRO_EXPANSION + std::cout << "expand " << name() << " " << locstring(rawtok->location) << std::endl; +#endif + + TokenList output2(inputFiles); + + if (functionLike() && rawtok->next && rawtok->next->op == '(') { + // Copy macro call to a new tokenlist with no linebreaks + const Token * const rawtok1 = rawtok; + TokenList rawtokens2(inputFiles); + rawtokens2.push_back(new Token(rawtok->str(), rawtok1->location, rawtok->whitespaceahead)); + rawtok = rawtok->next; + rawtokens2.push_back(new Token(rawtok->str(), rawtok1->location, rawtok->whitespaceahead)); + rawtok = rawtok->next; + int par = 1; + while (rawtok && par > 0) { + if (rawtok->op == '(') + ++par; + else if (rawtok->op == ')') + --par; + else if (rawtok->op == '#' && !sameline(rawtok->previous, rawtok)) + throw Error(rawtok->location, "it is invalid to use a preprocessor directive as macro parameter"); + rawtokens2.push_back(new Token(rawtok->str(), rawtok1->location, rawtok->whitespaceahead)); + rawtok = rawtok->next; + } + if (expand(output2, rawtok1->location, rawtokens2.cfront(), macros, expandedmacros)) + rawtok = rawtok1->next; + } else { + rawtok = expand(output2, rawtok->location, rawtok, macros, expandedmacros); + } + while (output2.cback() && rawtok) { + unsigned int par = 0; + Token* macro2tok = output2.back(); + while (macro2tok) { + if (macro2tok->op == '(') { + if (par==0) + break; + --par; + } else if (macro2tok->op == ')') { + ++par; + } + macro2tok = macro2tok->previous; + } + if (macro2tok) { // macro2tok->op == '(' + macro2tok = macro2tok->previous; + expandedmacros.insert(name()); + } else if (rawtok->op == '(') { + macro2tok = output2.back(); + } + if (!macro2tok || !macro2tok->name) + break; + if (output2.cfront() != output2.cback() && macro2tok->str() == this->name()) + break; + const MacroMap::const_iterator macro = macros.find(macro2tok->str()); + if (macro == macros.end() || !macro->second.functionLike()) + break; + TokenList rawtokens2(inputFiles); + const Location loc(macro2tok->location); + while (macro2tok) { + Token * const next = macro2tok->next; + rawtokens2.push_back(new Token(macro2tok->str(), loc)); + output2.deleteToken(macro2tok); + macro2tok = next; + } + par = (rawtokens2.cfront() != rawtokens2.cback()) ? 1U : 0U; + const Token *rawtok2 = rawtok; + for (; rawtok2; rawtok2 = rawtok2->next) { + rawtokens2.push_back(new Token(rawtok2->str(), loc)); + if (rawtok2->op == '(') { + ++par; + } + else if (rawtok2->op == ')') { + if (par <= 1U) + break; + --par; + } + } + if (!rawtok2 || par != 1U) + break; + if (macro->second.expand(output2, rawtok->location, rawtokens2.cfront(), macros, expandedmacros) != nullptr) + break; + rawtok = rawtok2->next; + } + output.takeTokens(output2); + return rawtok; + } + + /** macro name */ + const TokenString &name() const { + return nameTokDef->str(); + } + + /** location for macro definition */ + const Location &defineLocation() const { + return nameTokDef->location; + } + + /** how has this macro been used so far */ + const std::list &usage() const { + return usageList; + } + + /** is this a function like macro */ + bool functionLike() const { + return nameTokDef->next && + nameTokDef->next->op == '(' && + sameline(nameTokDef, nameTokDef->next) && + nameTokDef->next->location.col == nameTokDef->location.col + nameTokDef->str().size(); + } + + /** base class for errors */ + struct Error { + Error(const Location &loc, const std::string &s) : location(loc), what(s) {} + const Location location; + const std::string what; + }; + + /** Struct that is thrown when macro is expanded with wrong number of parameters */ + struct wrongNumberOfParameters : public Error { + wrongNumberOfParameters(const Location &loc, const std::string ¯oName) : Error(loc, "Wrong number of parameters for macro \'" + macroName + "\'.") {} + }; + + /** Struct that is thrown when there is invalid ## usage */ + struct invalidHashHash : public Error { + static inline std::string format(const std::string ¯oName, const std::string &message) { + return "Invalid ## usage when expanding \'" + macroName + "\': " + message; + } + + invalidHashHash(const Location &loc, const std::string ¯oName, const std::string &message) + : Error(loc, format(macroName, message)) {} + + static inline invalidHashHash unexpectedToken(const Location &loc, const std::string ¯oName, const Token *tokenA) { + return {loc, macroName, "Unexpected token '"+ tokenA->str()+"'"}; + } + + static inline invalidHashHash cannotCombine(const Location &loc, const std::string ¯oName, const Token *tokenA, const Token *tokenB) { + return {loc, macroName, "Combining '"+ tokenA->str()+ "' and '"+ tokenB->str() + "' yields an invalid token."}; + } + + static inline invalidHashHash unexpectedNewline(const Location &loc, const std::string ¯oName) { + return {loc, macroName, "Unexpected newline"}; + } + + static inline invalidHashHash universalCharacterUB(const Location &loc, const std::string ¯oName, const Token* tokenA, const std::string& strAB) { + return {loc, macroName, "Combining '\\"+ tokenA->str()+ "' and '"+ strAB.substr(tokenA->str().size()) + "' yields universal character '\\" + strAB + "'. This is undefined behavior according to C standard chapter 5.1.1.2, paragraph 4."}; + } + }; + private: + /** Create new token where Token::macro is set for replaced tokens */ + Token *newMacroToken(const TokenString &str, const Location &loc, bool replaced, const Token *expandedFromToken=nullptr) const { + auto *tok = new Token(str,loc); + if (replaced) + tok->macro = nameTokDef->str(); + if (expandedFromToken) + tok->setExpandedFrom(expandedFromToken, this); + return tok; + } + + bool parseDefine(const Token *nametoken) { + nameTokDef = nametoken; + variadic = false; + variadicOpt = false; + delete optExpandValue; + optExpandValue = nullptr; + delete optNoExpandValue; + optNoExpandValue = nullptr; + if (!nameTokDef) { + valueToken = endToken = nullptr; + args.clear(); + return false; + } + + // function like macro.. + if (functionLike()) { + args.clear(); + const Token *argtok = nameTokDef->next->next; + while (sameline(nametoken, argtok) && argtok->op != ')') { + if (argtok->str() == "..." && + argtok->next && argtok->next->op == ')') { + variadic = true; + if (!argtok->previous->name) + args.emplace_back("__VA_ARGS__"); + argtok = argtok->next; // goto ')' + break; + } + if (argtok->op != ',') + args.emplace_back(argtok->str()); + argtok = argtok->next; + } + if (!sameline(nametoken, argtok)) { + endToken = argtok ? argtok->previous : argtok; + valueToken = nullptr; + return false; + } + valueToken = argtok ? argtok->next : nullptr; + } else { + args.clear(); + valueToken = nameTokDef->next; + } + + if (!sameline(valueToken, nameTokDef)) + valueToken = nullptr; + endToken = valueToken; + while (sameline(endToken, nameTokDef)) { + if (variadic && endToken->str() == "__VA_OPT__") + variadicOpt = true; + endToken = endToken->next; + } + + if (variadicOpt) { + TokenList expandValue(files); + TokenList noExpandValue(files); + for (const Token *tok = valueToken; tok && tok != endToken;) { + if (tok->str() == "__VA_OPT__") { + if (!sameline(tok, tok->next) || tok->next->op != '(') + throw Error(tok->location, "In definition of '" + nameTokDef->str() + "': Missing opening parenthesis for __VA_OPT__"); + tok = tok->next->next; + int par = 1; + while (tok && tok != endToken) { + if (tok->op == '(') + par++; + else if (tok->op == ')') + par--; + else if (tok->str() == "__VA_OPT__") + throw Error(tok->location, "In definition of '" + nameTokDef->str() + "': __VA_OPT__ cannot be nested"); + if (par == 0) { + tok = tok->next; + break; + } + expandValue.push_back(new Token(*tok)); + tok = tok->next; + } + if (par != 0) { + const Token *const lastTok = expandValue.back() ? expandValue.back() : valueToken->next; + throw Error(lastTok->location, "In definition of '" + nameTokDef->str() + "': Missing closing parenthesis for __VA_OPT__"); + } + } else { + expandValue.push_back(new Token(*tok)); + noExpandValue.push_back(new Token(*tok)); + tok = tok->next; + } + } + optExpandValue = new TokenList(std::move(expandValue)); + optNoExpandValue = new TokenList(std::move(noExpandValue)); + } + + return true; + } + + unsigned int getArgNum(const TokenString &str) const { + unsigned int par = 0; + while (par < args.size()) { + if (str == args[par]) + return par; + par++; + } + return ~0U; + } + + std::vector getMacroParameters(const Token *nameTokInst, bool calledInDefine) const { + if (!nameTokInst->next || nameTokInst->next->op != '(' || !functionLike()) + return {}; + + std::vector parametertokens; + parametertokens.emplace_back(nameTokInst->next); + unsigned int par = 0U; + for (const Token *tok = nameTokInst->next->next; calledInDefine ? sameline(tok, nameTokInst) : (tok != nullptr); tok = tok->next) { + if (tok->op == '(') { + ++par; + } + else if (tok->op == ')') { + if (par == 0U) { + parametertokens.emplace_back(tok); + break; + } + --par; + } else if (par == 0U && tok->op == ',' && (!variadic || parametertokens.size() < args.size())) { + parametertokens.emplace_back(tok); + } + } + return parametertokens; + } + + const Token *appendTokens(TokenList &tokens, + const Location &rawloc, + const Token * const lpar, + const MacroMap ¯os, + const std::set &expandedmacros, + const std::vector ¶metertokens) const { + if (!lpar || lpar->op != '(') + return nullptr; + unsigned int par = 0; + const Token *tok = lpar; + while (sameline(lpar, tok)) { + if (tok->op == '#' && sameline(tok,tok->next) && tok->next->op == '#' && sameline(tok,tok->next->next)) { + // A##B => AB + tok = expandHashHash(tokens, rawloc, tok, macros, expandedmacros, parametertokens, false); + } else if (tok->op == '#' && sameline(tok, tok->next) && tok->next->op != '#') { + tok = expandHash(tokens, rawloc, tok, expandedmacros, parametertokens); + } else { + if (!expandArg(tokens, tok, rawloc, macros, expandedmacros, parametertokens)) { + tokens.push_back(new Token(*tok)); + if (tok->macro.empty() && (par > 0 || tok->str() != "(")) + tokens.back()->macro = name(); + } + + if (tok->op == '(') { + ++par; + } + else if (tok->op == ')') { + --par; + if (par == 0U) + break; + } + tok = tok->next; + } + } + for (Token *tok2 = tokens.front(); tok2; tok2 = tok2->next) + tok2->location = lpar->location; + return sameline(lpar,tok) ? tok : nullptr; + } + + const Token * expand(TokenList & output, const Location &loc, const Token * const nameTokInst, const MacroMap ¯os, std::set expandedmacros) const { + expandedmacros.insert(nameTokInst->str()); + +#ifdef SIMPLECPP_DEBUG_MACRO_EXPANSION + std::cout << " expand " << name() << " " << locstring(defineLocation()) << std::endl; +#endif + + usageList.emplace_back(loc); + + if (nameTokInst->str() == "__FILE__") { + output.push_back(new Token('\"'+output.file(loc)+'\"', loc)); + return nameTokInst->next; + } + if (nameTokInst->str() == "__LINE__") { + output.push_back(new Token(toString(loc.line), loc)); + return nameTokInst->next; + } + if (nameTokInst->str() == "__COUNTER__") { + output.push_back(new Token(toString(usageList.size()-1U), loc)); + return nameTokInst->next; + } + + const bool calledInDefine = (loc.fileIndex != nameTokInst->location.fileIndex || + loc.line < nameTokInst->location.line); + + std::vector parametertokens1(getMacroParameters(nameTokInst, calledInDefine)); + + if (functionLike()) { + // No arguments => not macro expansion + if (nameTokInst->next && nameTokInst->next->op != '(') { + output.push_back(new Token(nameTokInst->str(), loc)); + return nameTokInst->next; + } + + // Parse macro-call + if (variadic) { + if (parametertokens1.size() < args.size()) { + throw wrongNumberOfParameters(nameTokInst->location, name()); + } + } else { + if (parametertokens1.size() != args.size() + (args.empty() ? 2U : 1U)) + throw wrongNumberOfParameters(nameTokInst->location, name()); + } + } + + // If macro call uses __COUNTER__ then expand that first + TokenList tokensparams(files); + std::vector parametertokens2; + if (!parametertokens1.empty()) { + bool counter = false; + for (const Token *tok = parametertokens1[0]; tok != parametertokens1.back(); tok = tok->next) { + if (tok->str() == "__COUNTER__") { + counter = true; + break; + } + } + + const MacroMap::const_iterator m = macros.find("__COUNTER__"); + + if (!counter || m == macros.end()) { + parametertokens2.swap(parametertokens1); + } + else { + const Macro &counterMacro = m->second; + unsigned int par = 0; + for (const Token *tok = parametertokens1[0]; tok && par < parametertokens1.size(); tok = tok->next) { + if (tok->str() == "__COUNTER__") { + tokensparams.push_back(new Token(toString(counterMacro.usageList.size()), tok->location)); + counterMacro.usageList.emplace_back(tok->location); + } else { + tokensparams.push_back(new Token(*tok)); + if (tok == parametertokens1[par]) { + parametertokens2.emplace_back(tokensparams.cback()); + par++; + } + } + } + } + } + + // NOLINTNEXTLINE(misc-const-correctness) - technically correct but used to access non-const data + Token * const output_end_1 = output.back(); + + const Token *valueToken2; + const Token *endToken2; + + if (variadicOpt) { + if (parametertokens2.size() > args.size() && parametertokens2[args.size() - 1]->next->op != ')') + valueToken2 = optExpandValue->cfront(); + else + valueToken2 = optNoExpandValue->cfront(); + endToken2 = nullptr; + } else { + valueToken2 = valueToken; + endToken2 = endToken; + } + + // expand + for (const Token *tok = valueToken2; tok != endToken2;) { + if (tok->op != '#') { + // A##B => AB + if (sameline(tok, tok->next) && tok->next && tok->next->op == '#' && tok->next->next && tok->next->next->op == '#') { + if (!sameline(tok, tok->next->next->next)) + throw invalidHashHash::unexpectedNewline(tok->location, name()); + if (variadic && tok->op == ',' && tok->next->next->next->str() == args.back()) { + Token *const comma = newMacroToken(tok->str(), loc, isReplaced(expandedmacros), tok); + output.push_back(comma); + tok = expandToken(output, loc, tok->next->next->next, macros, expandedmacros, parametertokens2); + if (output.back() == comma) + output.deleteToken(comma); + continue; + } + TokenList new_output(files); + if (!expandArg(new_output, tok, parametertokens2)) + output.push_back(newMacroToken(tok->str(), loc, isReplaced(expandedmacros), tok)); + else if (new_output.empty()) // placemarker token + output.push_back(newMacroToken("", loc, isReplaced(expandedmacros))); + else + for (const Token *tok2 = new_output.cfront(); tok2; tok2 = tok2->next) + output.push_back(newMacroToken(tok2->str(), loc, isReplaced(expandedmacros), tok2)); + tok = tok->next; + } else { + tok = expandToken(output, loc, tok, macros, expandedmacros, parametertokens2); + } + continue; + } + + int numberOfHash = 1; + const Token *hashToken = tok->next; + while (sameline(tok,hashToken) && hashToken->op == '#') { + hashToken = hashToken->next; + ++numberOfHash; + } + if (numberOfHash == 4 && tok->next->location.col + 1 == tok->next->next->location.col) { + // # ## # => ## + output.push_back(newMacroToken("##", loc, isReplaced(expandedmacros))); + tok = hashToken; + continue; + } + + if (numberOfHash >= 2 && tok->location.col + 1 < tok->next->location.col) { + output.push_back(new Token(*tok)); + tok = tok->next; + continue; + } + + tok = tok->next; + if (tok == endToken2) { + if (tok) { + output.push_back(new Token(*tok->previous)); + } + else { + output.push_back(new Token(*nameTokInst)); + output.back()->setstr("\"\""); + } + break; + } + if (tok->op == '#') { + // A##B => AB + tok = expandHashHash(output, loc, tok->previous, macros, expandedmacros, parametertokens2); + } else { + // #123 => "123" + tok = expandHash(output, loc, tok->previous, expandedmacros, parametertokens2); + } + } + + if (!functionLike()) { + for (Token *tok = output_end_1 ? output_end_1->next : output.front(); tok; tok = tok->next) { + tok->macro = nameTokInst->str(); + } + } + + if (!parametertokens1.empty()) + parametertokens1.swap(parametertokens2); + + return functionLike() ? parametertokens2.back()->next : nameTokInst->next; + } + + const Token *recursiveExpandToken(TokenList &output, TokenList &temp, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens) const { + if (!temp.cback() || !temp.cback()->name || !tok->next || tok->next->op != '(') { + output.takeTokens(temp); + return tok->next; + } + + if (!sameline(tok, tok->next)) { + output.takeTokens(temp); + return tok->next; + } + + const MacroMap::const_iterator it = macros.find(temp.cback()->str()); + if (it == macros.end() || expandedmacros.find(temp.cback()->str()) != expandedmacros.end()) { + output.takeTokens(temp); + return tok->next; + } + + const Macro &calledMacro = it->second; + if (!calledMacro.functionLike()) { + output.takeTokens(temp); + return tok->next; + } + + TokenList temp2(files); + temp2.push_back(new Token(temp.cback()->str(), tok->location)); + + const Token * const tok2 = appendTokens(temp2, loc, tok->next, macros, expandedmacros, parametertokens); + if (!tok2) + return tok->next; + output.takeTokens(temp); + output.deleteToken(output.back()); + calledMacro.expand(output, loc, temp2.cfront(), macros, expandedmacros); + return tok2->next; + } + + const Token *expandToken(TokenList &output, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens) const { + // Not name.. + if (!tok->name) { + output.push_back(newMacroToken(tok->str(), loc, true, tok)); + return tok->next; + } + + // Macro parameter.. + { + TokenList temp(files); + if (expandArg(temp, tok, loc, macros, expandedmacros, parametertokens)) { + if (tok->str() == "__VA_ARGS__" && temp.empty() && output.cback() && output.cback()->str() == "," && + tok->nextSkipComments() && tok->nextSkipComments()->str() == ")") + output.deleteToken(output.back()); + return recursiveExpandToken(output, temp, loc, tok, macros, expandedmacros, parametertokens); + } + } + + // Macro.. + const MacroMap::const_iterator it = macros.find(tok->str()); + if (it != macros.end() && expandedmacros.find(tok->str()) == expandedmacros.end()) { + std::set expandedmacros2(expandedmacros); + expandedmacros2.insert(tok->str()); + + const Macro &calledMacro = it->second; + if (!calledMacro.functionLike()) { + TokenList temp(files); + calledMacro.expand(temp, loc, tok, macros, expandedmacros); + return recursiveExpandToken(output, temp, loc, tok, macros, expandedmacros2, parametertokens); + } + if (!sameline(tok, tok->next)) { + output.push_back(newMacroToken(tok->str(), loc, true, tok)); + return tok->next; + } + TokenList tokens(files); + tokens.push_back(new Token(*tok)); + const Token * tok2 = nullptr; + if (tok->next->op == '(') { + tok2 = appendTokens(tokens, loc, tok->next, macros, expandedmacros, parametertokens); + } + else if (expandArg(tokens, tok->next, loc, macros, expandedmacros, parametertokens)) { + tokens.front()->location = loc; + if (tokens.cfront()->next && tokens.cfront()->next->op == '(') + tok2 = tok->next; + } + if (!tok2) { + output.push_back(newMacroToken(tok->str(), loc, true, tok)); + return tok->next; + } + TokenList temp(files); + calledMacro.expand(temp, loc, tokens.cfront(), macros, expandedmacros); + return recursiveExpandToken(output, temp, loc, tok2, macros, expandedmacros, parametertokens); + } + + if (tok->str() == DEFINED) { + const Token * const tok2 = tok->next; + const Token * const tok3 = tok2 ? tok2->next : nullptr; + const Token * const tok4 = tok3 ? tok3->next : nullptr; + const Token *defToken = nullptr; + const Token *lastToken = nullptr; + if (sameline(tok, tok4) && tok2->op == '(' && tok3->name && tok4->op == ')') { + defToken = tok3; + lastToken = tok4; + } else if (sameline(tok,tok2) && tok2->name) { + defToken = lastToken = tok2; + } + if (defToken) { + std::string macroName = defToken->str(); + if (defToken->next && defToken->next->op == '#' && defToken->next->next && defToken->next->next->op == '#' && defToken->next->next->next && defToken->next->next->next->name && sameline(defToken,defToken->next->next->next)) { + TokenList temp(files); + if (expandArg(temp, defToken, parametertokens)) + macroName = temp.cback()->str(); + if (expandArg(temp, defToken->next->next->next, parametertokens)) + macroName += temp.cback() ? temp.cback()->str() : ""; + else + macroName += defToken->next->next->next->str(); + lastToken = defToken->next->next->next; + } + const bool def = (macros.find(macroName) != macros.end()); + output.push_back(newMacroToken(def ? "1" : "0", loc, true)); + return lastToken->next; + } + } + + output.push_back(newMacroToken(tok->str(), loc, true, tok)); + return tok->next; + } + + bool expandArg(TokenList &output, const Token *tok, const std::vector ¶metertokens) const { + if (!tok->name) + return false; + + const unsigned int argnr = getArgNum(tok->str()); + if (argnr >= args.size()) + return false; + + // empty variadic parameter + if (variadic && argnr + 1U >= parametertokens.size()) + return true; + + for (const Token *partok = parametertokens[argnr]->next; partok != parametertokens[argnr + 1U]; partok = partok->next) + output.push_back(new Token(*partok)); + + return true; + } + + bool expandArg(TokenList &output, const Token *tok, const Location &loc, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens) const { + if (!tok->name) + return false; + const unsigned int argnr = getArgNum(tok->str()); + if (argnr >= args.size()) + return false; + if (variadic && argnr + 1U >= parametertokens.size()) // empty variadic parameter + return true; + for (const Token *partok = parametertokens[argnr]->next; partok != parametertokens[argnr + 1U];) { + const MacroMap::const_iterator it = macros.find(partok->str()); + if (it != macros.end() && !partok->isExpandedFrom(&it->second) && (partok->str() == name() || expandedmacros.find(partok->str()) == expandedmacros.end())) { + std::set expandedmacros2(expandedmacros); // temporary amnesia to allow reexpansion of currently expanding macros during argument evaluation + expandedmacros2.erase(name()); + partok = it->second.expand(output, loc, partok, macros, std::move(expandedmacros2)); + } else { + output.push_back(newMacroToken(partok->str(), loc, isReplaced(expandedmacros), partok)); + output.back()->macro = partok->macro; + partok = partok->next; + } + } + if (tok->whitespaceahead && output.back()) + output.back()->whitespaceahead = true; + return true; + } + + /** + * Expand #X => "X" + * @param output destination tokenlist + * @param loc location for expanded token + * @param tok The # token + * @param expandedmacros set with expanded macros, with this macro + * @param parametertokens parameters given when expanding this macro + * @return token after the X + */ + const Token *expandHash(TokenList &output, const Location &loc, const Token *tok, const std::set &expandedmacros, const std::vector ¶metertokens) const { + TokenList tokenListHash(files); + const MacroMap macros2; // temporarily bypass macro expansion + tok = expandToken(tokenListHash, loc, tok->next, macros2, expandedmacros, parametertokens); + std::ostringstream ostr; + ostr << '\"'; + for (const Token *hashtok = tokenListHash.cfront(), *next; hashtok; hashtok = next) { + next = hashtok->next; + ostr << hashtok->str(); + if (next && hashtok->whitespaceahead) + ostr << ' '; + } + ostr << '\"'; + output.push_back(newMacroToken(escapeString(ostr.str()), loc, isReplaced(expandedmacros))); + return tok; + } + + /** + * Expand A##B => AB + * The A should already be expanded. Call this when you reach the first # token + * @param output destination tokenlist + * @param loc location for expanded token + * @param tok first # token + * @param macros all macros + * @param expandedmacros set with expanded macros, with this macro + * @param parametertokens parameters given when expanding this macro + * @param expandResult expand ## result i.e. "AB"? + * @return token after B + */ + const Token *expandHashHash(TokenList &output, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, bool expandResult=true) const { + Token *A = output.back(); + if (!A) + throw invalidHashHash(tok->location, name(), "Missing first argument"); + if (!sameline(tok, tok->next) || !sameline(tok, tok->next->next)) + throw invalidHashHash::unexpectedNewline(tok->location, name()); + + const bool canBeConcatenatedWithEqual = A->isOneOf("+-*/%&|^") || A->str() == "<<" || A->str() == ">>"; + const bool canBeConcatenatedStringOrChar = isStringLiteral(A->str()) || isCharLiteral(A->str()); + const bool unexpectedA = (!A->name && !A->number && !A->str().empty() && !canBeConcatenatedWithEqual && !canBeConcatenatedStringOrChar); + + const Token * const B = tok->next->next; + if (!B->name && !B->number && B->op && !B->isOneOf("#=")) + throw invalidHashHash::unexpectedToken(tok->location, name(), B); + + if ((canBeConcatenatedWithEqual && B->op != '=') || + (!canBeConcatenatedWithEqual && B->op == '=')) + throw invalidHashHash::cannotCombine(tok->location, name(), A, B); + + // Superficial check; more in-depth would in theory be possible _after_ expandArg + if (canBeConcatenatedStringOrChar && (B->number || !B->name)) + throw invalidHashHash::cannotCombine(tok->location, name(), A, B); + + TokenList tokensB(files); + const Token *nextTok = B->next; + + if (canBeConcatenatedStringOrChar) { + if (unexpectedA) + throw invalidHashHash::unexpectedToken(tok->location, name(), A); + + // It seems clearer to handle this case separately even though the code is similar-ish, but we don't want to merge here. + // TODO The question is whether the ## or varargs may still apply, and how to provoke? + if (expandArg(tokensB, B, parametertokens)) { + for (Token *b = tokensB.front(); b; b = b->next) + b->location = loc; + } else { + tokensB.push_back(new Token(*B)); + tokensB.back()->location = loc; + } + output.takeTokens(tokensB); + } else { + std::string strAB; + + const bool varargs = variadic && !args.empty() && B->str() == args[args.size()-1U]; + + if (expandArg(tokensB, B, parametertokens)) { + if (tokensB.empty()) { + strAB = A->str(); + } + else if (varargs && A->op == ',') { + strAB = ","; + } + else if (varargs && unexpectedA) { + throw invalidHashHash::unexpectedToken(tok->location, name(), A); + } + else { + strAB = A->str() + tokensB.cfront()->str(); + tokensB.deleteToken(tokensB.front()); + } + } else { + if (unexpectedA) + throw invalidHashHash::unexpectedToken(tok->location, name(), A); + strAB = A->str() + B->str(); + } + + // producing universal character is undefined behavior + if (A->previous && A->previous->str() == "\\") { + if (strAB[0] == 'u' && strAB.size() == 5) + throw invalidHashHash::universalCharacterUB(tok->location, name(), A, strAB); + if (strAB[0] == 'U' && strAB.size() == 9) + throw invalidHashHash::universalCharacterUB(tok->location, name(), A, strAB); + } + + if (varargs && tokensB.empty() && tok->previous->str() == ",") { + output.deleteToken(A); + } + else if (strAB != "," && macros.find(strAB) == macros.end()) { + A->setstr(strAB); + for (Token *b = tokensB.front(); b; b = b->next) + b->location = loc; + output.takeTokens(tokensB); + } else if (sameline(B, nextTok) && sameline(B, nextTok->next) && nextTok->op == '#' && nextTok->next->op == '#') { + TokenList output2(files); + output2.push_back(new Token(strAB, tok->location)); + nextTok = expandHashHash(output2, loc, nextTok, macros, expandedmacros, parametertokens); + output.deleteToken(A); + output.takeTokens(output2); + } else { + output.deleteToken(A); + TokenList tokens(files); + tokens.push_back(new Token(strAB, tok->location)); + // for function like macros, push the (...) + if (tokensB.empty() && sameline(B,B->next) && B->next->op=='(') { + const MacroMap::const_iterator it = macros.find(strAB); + if (it != macros.end() && expandedmacros.find(strAB) == expandedmacros.end() && it->second.functionLike()) { + const Token * const tok2 = appendTokens(tokens, loc, B->next, macros, expandedmacros, parametertokens); + if (tok2) + nextTok = tok2->next; + } + } + if (expandResult) + expandToken(output, loc, tokens.cfront(), macros, expandedmacros, parametertokens); + else + output.takeTokens(tokens); + for (Token *b = tokensB.front(); b; b = b->next) + b->location = loc; + output.takeTokens(tokensB); + } + } + + return nextTok; + } + + static bool isReplaced(const std::set &expandedmacros) { + // return true if size > 1 + auto it = expandedmacros.cbegin(); + if (it == expandedmacros.cend()) + return false; + ++it; + return (it != expandedmacros.cend()); + } + + /** name token in definition */ + const Token *nameTokDef; + + /** arguments for macro */ + std::vector args; + + /** first token in replacement string */ + const Token *valueToken; + + /** token after replacement string */ + const Token *endToken; + + /** files */ + std::vector &files; + + /** this is used for -D where the definition is not seen anywhere in code */ + TokenList tokenListDefine; + + /** usage of this macro */ + mutable std::list usageList; + + /** is macro variadic? */ + bool variadic; + + /** does the macro expansion have __VA_OPT__? */ + bool variadicOpt; + + /** Expansion value for varadic macros with __VA_OPT__ expanded and discarded respectively */ + const TokenList *optExpandValue{}; + const TokenList *optNoExpandValue{}; + + /** was the value of this macro actually defined in the code? */ + bool valueDefinedInCode_; + }; +} + +namespace simplecpp { + +#ifdef __CYGWIN__ + static bool startsWith(const std::string &s, const std::string &p) + { + return (s.size() >= p.size()) && std::equal(p.begin(), p.end(), s.begin()); + } + + std::string convertCygwinToWindowsPath(const std::string &cygwinPath) + { + std::string windowsPath; + + std::string::size_type pos = 0; + if (cygwinPath.size() >= 11 && startsWith(cygwinPath, "/cygdrive/")) { + const unsigned char driveLetter = cygwinPath[10]; + if (std::isalpha(driveLetter)) { + if (cygwinPath.size() == 11) { + windowsPath = toupper(driveLetter); + windowsPath += ":\\"; // volume root directory + pos = 11; + } else if (cygwinPath[11] == '/') { + windowsPath = toupper(driveLetter); + windowsPath += ":"; + pos = 11; + } + } + } + + for (; pos < cygwinPath.size(); ++pos) { + unsigned char c = cygwinPath[pos]; + if (c == '/') + c = '\\'; + windowsPath += c; + } + + return windowsPath; + } +#endif + + bool isAbsolutePath(const std::string &path) + { +#ifdef SIMPLECPP_WINDOWS + // C:\\path\\file + // C:/path/file + if (path.length() >= 3 && std::isalpha(path[0]) && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) + return true; + + // \\host\path\file + // //host/path/file + if (path.length() >= 2 && (path[0] == '\\' || path[0] == '/') && (path[1] == '\\' || path[1] == '/')) + return true; + + return false; +#else + return !path.empty() && path[0] == '/'; +#endif + } +} + +namespace simplecpp { + /** + * perform path simplifications for . and .. + */ + std::string simplifyPath(std::string path) + { + if (path.empty()) + return path; + + std::string::size_type pos; + + // replace backslash separators + std::replace(path.begin(), path.end(), '\\', '/'); + + const bool unc(path.compare(0,2,"//") == 0); + + // replace "//" with "/" + pos = 0; + while ((pos = path.find("//",pos)) != std::string::npos) { + path.erase(pos,1); + } + + // remove "./" + pos = 0; + while ((pos = path.find("./",pos)) != std::string::npos) { + if (pos == 0 || path[pos - 1U] == '/') + path.erase(pos,2); + else + pos += 2; + } + + // remove trailing dot if path ends with "/." + if (endsWith(path,"/.")) + path.erase(path.size()-1); + + // simplify ".." + pos = 1; // don't simplify ".." if path starts with that + while ((pos = path.find("/..", pos)) != std::string::npos) { + // not end of path, then string must be "/../" + if (pos + 3 < path.size() && path[pos + 3] != '/') { + ++pos; + continue; + } + // get previous subpath + std::string::size_type pos1 = path.rfind('/', pos - 1U); + if (pos1 == std::string::npos) { + pos1 = 0; + } else { + pos1 += 1U; + } + const std::string previousSubPath = path.substr(pos1, pos - pos1); + if (previousSubPath == "..") { + // don't simplify + ++pos; + } else { + // remove previous subpath and ".." + path.erase(pos1, pos - pos1 + 4); + if (path.empty()) + path = "."; + // update pos + pos = (pos1 == 0) ? 1 : (pos1 - 1); + } + } + + // Remove trailing '/'? + //if (path.size() > 1 && endsWith(path, "/")) + // path.erase(path.size()-1); + + if (unc) + path = '/' + path; + + return path; + } +} + +/** Evaluate sizeof(type) + * @throws std::runtime_error thrown on missing arguments or invalid expression + */ +static void simplifySizeof(simplecpp::TokenList &expr, const std::map &sizeOfType) +{ + for (simplecpp::Token *tok = expr.front(); tok; tok = tok->next) { + if (tok->str() != "sizeof") + continue; + const simplecpp::Token *tok1 = tok->next; + if (!tok1) { + throw std::runtime_error("missing sizeof argument"); + } + const simplecpp::Token *tok2 = tok1->next; + if (!tok2) { + throw std::runtime_error("missing sizeof argument"); + } + if (tok1->op == '(') { + tok1 = tok1->next; + while (tok2->op != ')') { + tok2 = tok2->next; + if (!tok2) { + throw std::runtime_error("invalid sizeof expression"); + } + } + } + + std::string type; + for (const simplecpp::Token *typeToken = tok1; typeToken != tok2; typeToken = typeToken->next) { + if ((typeToken->str() == "unsigned" || typeToken->str() == "signed") && typeToken->next->name) + continue; + if (typeToken->str() == "*" && type.find('*') != std::string::npos) + continue; + if (!type.empty()) + type += ' '; + type += typeToken->str(); + } + + const std::map::const_iterator it = sizeOfType.find(type); + if (it != sizeOfType.end()) + tok->setstr(toString(it->second)); + else + continue; + + tok2 = tok2->next; + while (tok->next != tok2) + expr.deleteToken(tok->next); + } +} + +static bool isCpp17OrLater(const simplecpp::DUI &dui) +{ + const std::string std_ver = simplecpp::getCppStdString(dui.std); + return std_ver.empty() || (std_ver >= "201703L"); +} + +static bool isGnu(const simplecpp::DUI &dui) +{ + return dui.std.rfind("gnu", 0) != std::string::npos; +} + +static std::string dirPath(const std::string& path, bool withTrailingSlash=true) +{ + const std::size_t lastSlash = path.find_last_of("\\/"); + if (lastSlash == std::string::npos) { + return ""; + } + return path.substr(0, lastSlash + (withTrailingSlash ? 1U : 0U)); +} + +static std::string openHeader(std::ifstream &f, const simplecpp::DUI &dui, const std::string &sourcefile, const std::string &header, bool systemheader); + +/** Evaluate __has_include(include) + * @throws std::runtime_error thrown on missing arguments or invalid expression + */ +static void simplifyHasInclude(simplecpp::TokenList &expr, const simplecpp::DUI &dui) +{ + if (!isCpp17OrLater(dui) && !isGnu(dui)) + return; + + for (simplecpp::Token *tok = expr.front(); tok; tok = tok->next) { + if (tok->str() != HAS_INCLUDE) + continue; + const simplecpp::Token *tok1 = tok->next; + if (!tok1) { + throw std::runtime_error("missing __has_include argument"); + } + const simplecpp::Token *tok2 = tok1->next; + if (!tok2) { + throw std::runtime_error("missing __has_include argument"); + } + if (tok1->op == '(') { + tok1 = tok1->next; + while (tok2->op != ')') { + tok2 = tok2->next; + if (!tok2) { + throw std::runtime_error("invalid __has_include expression"); + } + } + } + + const std::string &sourcefile = expr.file(tok->location); + const bool systemheader = (tok1 && tok1->op == '<'); + std::string header; + if (systemheader) { + const simplecpp::Token *tok3 = tok1->next; + if (!tok3) { + throw std::runtime_error("missing __has_include closing angular bracket"); + } + while (tok3->op != '>') { + tok3 = tok3->next; + if (!tok3) { + throw std::runtime_error("invalid __has_include expression"); + } + } + + for (const simplecpp::Token *headerToken = tok1->next; headerToken != tok3; headerToken = headerToken->next) + header += headerToken->str(); + } else { + header = tok1->str().substr(1U, tok1->str().size() - 2U); + } + std::ifstream f; + const std::string header2 = openHeader(f,dui,sourcefile,header,systemheader); + tok->setstr(header2.empty() ? "0" : "1"); + + tok2 = tok2->next; + while (tok->next != tok2) + expr.deleteToken(tok->next); + } +} + +/** Evaluate name + * @throws std::runtime_error thrown on undefined function-like macro + */ +static void simplifyName(simplecpp::TokenList &expr) +{ + for (simplecpp::Token *tok = expr.front(); tok; tok = tok->next) { + if (tok->name) { + static const std::set altop{"and","or","bitand","bitor","compl","not","not_eq","xor"}; + if (altop.find(tok->str()) != altop.end()) { + bool alt; + if (tok->str() == "not" || tok->str() == "compl") { + alt = isAlternativeUnaryOp(tok,tok->str()); + } else { + alt = isAlternativeBinaryOp(tok,tok->str()); + } + if (alt) + continue; + } + if (tok->next && tok->next->str() == "(") + throw std::runtime_error("undefined function-like macro invocation: " + tok->str() + "( ... )"); + tok->setstr("0"); + } + } +} + +/* + * Reads at least minlen and at most maxlen digits (inc. prefix) in base base + * from s starting at position pos and converts them to a + * unsigned long long value, updating pos to point to the first + * unused element of s. + * Returns ULLONG_MAX if the result is not representable and + * @throws std::runtime_error thrown if the above requirements were not possible to satisfy. + */ +static unsigned long long stringToULLbounded( + const std::string& s, + std::size_t& pos, + int base = 0, + std::ptrdiff_t minlen = 1, + std::size_t maxlen = std::string::npos + ) +{ + const std::string sub = s.substr(pos, maxlen); + const char * const start = sub.c_str(); + char* end; + const unsigned long long value = std::strtoull(start, &end, base); + pos += end - start; + if (end - start < minlen) + throw std::runtime_error("expected digit"); + return value; +} + +long long simplecpp::characterLiteralToLL(const std::string& str) +{ + // default is wide/utf32 + bool narrow = false; + bool utf8 = false; + bool utf16 = false; + + std::size_t pos; + + if (!str.empty() && str[0] == '\'') { + narrow = true; + pos = 1; + } else if (str.size() >= 2 && str[0] == 'u' && str[1] == '\'') { + utf16 = true; + pos = 2; + } else if (str.size() >= 3 && str[0] == 'u' && str[1] == '8' && str[2] == '\'') { + utf8 = true; + pos = 3; + } else if (str.size() >= 2 && (str[0] == 'L' || str[0] == 'U') && str[1] == '\'') { + pos = 2; + } else { + throw std::runtime_error("expected a character literal"); + } + + unsigned long long multivalue = 0; + + std::size_t nbytes = 0; + + while (pos + 1 < str.size()) { + if (str[pos] == '\'' || str[pos] == '\n') + throw std::runtime_error("raw single quotes and newlines not allowed in character literals"); + + if (nbytes >= 1 && !narrow) + throw std::runtime_error("multiple characters only supported in narrow character literals"); + + unsigned long long value; + + if (str[pos] == '\\') { + pos++; + const char escape = str[pos++]; + + if (pos >= str.size()) + throw std::runtime_error("unexpected end of character literal"); + + switch (escape) { + // obscure GCC extensions + case '%': + case '(': + case '[': + case '{': + // standard escape sequences + case '\'': + case '"': + case '?': + case '\\': + value = static_cast(escape); + break; + + case 'a': + value = static_cast('\a'); + break; + case 'b': + value = static_cast('\b'); + break; + case 'f': + value = static_cast('\f'); + break; + case 'n': + value = static_cast('\n'); + break; + case 'r': + value = static_cast('\r'); + break; + case 't': + value = static_cast('\t'); + break; + case 'v': + value = static_cast('\v'); + break; + + // GCC extension for ESC character + case 'e': + case 'E': + value = static_cast('\x1b'); + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + // octal escape sequences consist of 1 to 3 digits + value = stringToULLbounded(str, --pos, 8, 1, 3); + break; + + case 'x': + // hexadecimal escape sequences consist of at least 1 digit + value = stringToULLbounded(str, pos, 16); + break; + + case 'u': + case 'U': { + // universal character names have exactly 4 or 8 digits + const std::size_t ndigits = (escape == 'u' ? 4 : 8); + value = stringToULLbounded(str, pos, 16, ndigits, ndigits); + + // UTF-8 encodes code points above 0x7f in multiple code units + // code points above 0x10ffff are not allowed + if (((narrow || utf8) && value > 0x7f) || (utf16 && value > 0xffff) || value > 0x10ffff) + throw std::runtime_error("code point too large"); + + if (value >= 0xd800 && value <= 0xdfff) + throw std::runtime_error("surrogate code points not allowed in universal character names"); + + break; + } + + default: + throw std::runtime_error("invalid escape sequence"); + } + } else { + value = static_cast(str[pos++]); + + if (!narrow && value >= 0x80) { + // Assuming this is a UTF-8 encoded code point. + // This decoder may not completely validate the input. + // Noncharacters are neither rejected nor replaced. + + int additional_bytes; + if (value >= 0xf5) // higher values would result in code points above 0x10ffff + throw std::runtime_error("assumed UTF-8 encoded source, but sequence is invalid"); + if (value >= 0xf0) + additional_bytes = 3; + else if (value >= 0xe0) + additional_bytes = 2; + else if (value >= 0xc2) // 0xc0 and 0xc1 are always overlong 2-bytes encodings + additional_bytes = 1; + else + throw std::runtime_error("assumed UTF-8 encoded source, but sequence is invalid"); + + value &= (1 << (6 - additional_bytes)) - 1; + + while (additional_bytes--) { + if (pos + 1 >= str.size()) + throw std::runtime_error("assumed UTF-8 encoded source, but character literal ends unexpectedly"); + + const unsigned char c = str[pos++]; + + if (((c >> 6) != 2) // ensure c has form 0xb10xxxxxx + || (!value && additional_bytes == 1 && c < 0xa0) // overlong 3-bytes encoding + || (!value && additional_bytes == 2 && c < 0x90)) // overlong 4-bytes encoding + throw std::runtime_error("assumed UTF-8 encoded source, but sequence is invalid"); + + value = (value << 6) | (c & ((1 << 7) - 1)); + } + + if (value >= 0xd800 && value <= 0xdfff) + throw std::runtime_error("assumed UTF-8 encoded source, but sequence is invalid"); + + if ((utf8 && value > 0x7f) || (utf16 && value > 0xffff) || value > 0x10ffff) + throw std::runtime_error("code point too large"); + } + } + + if (((narrow || utf8) && value > std::numeric_limits::max()) || (utf16 && value >> 16) || value >> 32) + throw std::runtime_error("numeric escape sequence too large"); + + multivalue <<= CHAR_BIT; + multivalue |= value; + nbytes++; + } + + if (pos + 1 != str.size() || str[pos] != '\'') + throw std::runtime_error("missing closing quote in character literal"); + + if (!nbytes) + throw std::runtime_error("empty character literal"); + + // ordinary narrow character literal's value is determined by (possibly signed) char + if (narrow && nbytes == 1) + return static_cast(multivalue); + + // while multi-character literal's value is determined by (signed) int + if (narrow) + return static_cast(multivalue); + + // All other cases are unsigned. Since long long is at least 64bit wide, + // while the literals at most 32bit wide, the conversion preserves all values. + return multivalue; +} + +/** + * @throws std::runtime_error thrown on invalid literal + */ +static void simplifyNumbers(simplecpp::TokenList &expr) +{ + for (simplecpp::Token *tok = expr.front(); tok; tok = tok->next) { + if (tok->str().size() == 1U) + continue; + if (tok->str().compare(0,2,"0x") == 0) + tok->setstr(toString(stringToULL(tok->str()))); + else if (!tok->number && tok->str().find('\'') != std::string::npos) + tok->setstr(toString(simplecpp::characterLiteralToLL(tok->str()))); + } +} + +static void simplifyComments(simplecpp::TokenList &expr) +{ + for (simplecpp::Token *tok = expr.front(); tok;) { + simplecpp::Token * const d = tok; + tok = tok->next; + if (d->comment) + expr.deleteToken(d); + } +} + +/** + * @throws std::runtime_error thrown on invalid literals, missing sizeof arguments or invalid expressions, + * missing __has_include() arguments or expressions, undefined function-like macros, invalid number literals + * @throws std::overflow_error thrown on overflow or division by zero + */ +static long long evaluate(simplecpp::TokenList &expr, const simplecpp::DUI &dui, const std::map &sizeOfType) +{ + simplifyComments(expr); + simplifySizeof(expr, sizeOfType); + simplifyHasInclude(expr, dui); + simplifyName(expr); + simplifyNumbers(expr); + expr.constFold(); + // TODO: handle invalid expressions + return expr.cfront() && expr.cfront() == expr.cback() && expr.cfront()->number ? stringToLL(expr.cfront()->str()) : 0LL; +} + +static const simplecpp::Token *gotoNextLine(const simplecpp::Token *tok) +{ + const unsigned int line = tok->location.line; + const unsigned int file = tok->location.fileIndex; + while (tok && tok->location.line == line && tok->location.fileIndex == file) + tok = tok->next; + return tok; +} + +#ifdef SIMPLECPP_WINDOWS + +class NonExistingFilesCache { +public: + NonExistingFilesCache() {} + + bool contains(const std::string& path) { + std::lock_guard lock(m_mutex); + return (m_pathSet.find(path) != m_pathSet.end()); + } + + void add(const std::string& path) { + std::lock_guard lock(m_mutex); + m_pathSet.insert(path); + } + + void clear() { + std::lock_guard lock(m_mutex); + m_pathSet.clear(); + } + +private: + std::set m_pathSet; + std::mutex m_mutex; +}; + +static NonExistingFilesCache nonExistingFilesCache; + +#endif + +static std::string openHeaderDirect(std::ifstream &f, const std::string &path) +{ +#ifdef SIMPLECPP_WINDOWS + if (nonExistingFilesCache.contains(path)) + return ""; // file is known not to exist, skip expensive file open call +#endif + f.open(path.c_str()); + if (f.is_open()) + return path; +#ifdef SIMPLECPP_WINDOWS + nonExistingFilesCache.add(path); +#endif + return ""; +} + +static std::string openHeader(std::ifstream &f, const simplecpp::DUI &dui, const std::string &sourcefile, const std::string &header, bool systemheader) +{ + if (simplecpp::isAbsolutePath(header)) + return openHeaderDirect(f, simplecpp::simplifyPath(header)); + + // prefer first to search the header relatively to source file if found, when not a system header + if (!systemheader) { + std::string path = openHeaderDirect(f, simplecpp::simplifyPath(dirPath(sourcefile) + header)); + if (!path.empty()) { + return path; + } + } + + // search the header on the include paths (provided by the flags "-I...") + for (const auto &includePath : dui.includePaths) { + std::string path = openHeaderDirect(f, simplecpp::simplifyPath(includePath + "/" + header)); + if (!path.empty()) + return path; + } + return ""; +} + +std::pair simplecpp::FileDataCache::tryload(FileDataCache::name_map_type::iterator &name_it, const simplecpp::DUI &dui, std::vector &filenames, simplecpp::OutputList *outputList) +{ + const std::string &path = name_it->first; + FileID fileId; + + if (!getFileId(path, fileId)) + return {nullptr, false}; + + const auto id_it = mIdMap.find(fileId); + if (id_it != mIdMap.end()) { + name_it->second = id_it->second; + return {id_it->second, false}; + } + + auto *const data = new FileData {path, TokenList(path, filenames, outputList)}; + + if (dui.removeComments) + data->tokens.removeComments(); + + name_it->second = data; + mIdMap.emplace(fileId, data); + mData.emplace_back(data); + + return {data, true}; +} + +std::pair simplecpp::FileDataCache::get(const std::string &sourcefile, const std::string &header, const simplecpp::DUI &dui, bool systemheader, std::vector &filenames, simplecpp::OutputList *outputList) +{ + if (isAbsolutePath(header)) { + auto ins = mNameMap.emplace(simplecpp::simplifyPath(header), nullptr); + + if (ins.second) { + const auto ret = tryload(ins.first, dui, filenames, outputList); + if (ret.first != nullptr) { + return ret; + } + } else { + return {ins.first->second, false}; + } + + return {nullptr, false}; + } + + if (!systemheader) { + auto ins = mNameMap.emplace(simplecpp::simplifyPath(dirPath(sourcefile) + header), nullptr); + + if (ins.second) { + const auto ret = tryload(ins.first, dui, filenames, outputList); + if (ret.first != nullptr) { + return ret; + } + } else if (ins.first->second != nullptr) { + return {ins.first->second, false}; + } + } + + for (const auto &includePath : dui.includePaths) { + auto ins = mNameMap.emplace(simplecpp::simplifyPath(includePath + "/" + header), nullptr); + + if (ins.second) { + const auto ret = tryload(ins.first, dui, filenames, outputList); + if (ret.first != nullptr) { + return ret; + } + } else if (ins.first->second != nullptr) { + return {ins.first->second, false}; + } + } + + return {nullptr, false}; +} + +bool simplecpp::FileDataCache::getFileId(const std::string &path, FileID &id) +{ +#ifdef _WIN32 + HANDLE hFile = CreateFileA(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + + if (hFile == INVALID_HANDLE_VALUE) + return false; + + BOOL ret = GetFileInformationByHandleEx(hFile, FileIdInfo, &id.fileIdInfo, sizeof(id.fileIdInfo)); + if (!ret) { + const DWORD err = GetLastError(); + if (err == ERROR_INVALID_PARAMETER || // encountered when using a non-NTFS filesystem e.g. exFAT + err == ERROR_NOT_SUPPORTED) // encountered on Windows Server Core (used as a Docker container) + { + BY_HANDLE_FILE_INFORMATION fileInfo; + ret = GetFileInformationByHandle(hFile, &fileInfo); + if (ret) { + id.fileIdInfo.VolumeSerialNumber = static_cast(fileInfo.dwVolumeSerialNumber); + id.fileIdInfo.FileId.IdentifierHi = static_cast(fileInfo.nFileIndexHigh); + id.fileIdInfo.FileId.IdentifierLo = static_cast(fileInfo.nFileIndexLow); + } + } + } + + CloseHandle(hFile); + + return ret == TRUE; +#else + struct stat statbuf; + + if (stat(path.c_str(), &statbuf) != 0) + return false; + + id.dev = statbuf.st_dev; + id.ino = statbuf.st_ino; + + return true; +#endif +} + +simplecpp::FileDataCache simplecpp::load(const simplecpp::TokenList &rawtokens, std::vector &filenames, const simplecpp::DUI &dui, simplecpp::OutputList *outputList, FileDataCache cache) +{ +#ifdef SIMPLECPP_WINDOWS + if (dui.clearIncludeCache) + nonExistingFilesCache.clear(); +#endif + + std::list filelist; + + // -include files + for (auto it = dui.includes.cbegin(); it != dui.includes.cend(); ++it) { + const std::string &filename = *it; + + const auto loadResult = cache.get("", filename, dui, false, filenames, outputList); + const bool loaded = loadResult.second; + FileData *const filedata = loadResult.first; + + if (filedata == nullptr) { + if (outputList) { + simplecpp::Output err{ + simplecpp::Output::EXPLICIT_INCLUDE_NOT_FOUND, + {}, + "Can not open include file '" + filename + "' that is explicitly included." + }; + outputList->emplace_back(std::move(err)); + } + continue; + } + + if (!loaded) + continue; + + if (!filedata->tokens.front()) + continue; + + if (dui.removeComments) + filedata->tokens.removeComments(); + + filelist.emplace_back(filedata->tokens.front()); + } + + for (const Token *rawtok = rawtokens.cfront(); rawtok || !filelist.empty(); rawtok = rawtok ? rawtok->next : nullptr) { + if (rawtok == nullptr) { + rawtok = filelist.back(); + filelist.pop_back(); + } + + if (rawtok->op != '#' || sameline(rawtok->previousSkipComments(), rawtok)) + continue; + + rawtok = rawtok->nextSkipComments(); + if (!rawtok || rawtok->str() != INCLUDE) + continue; + + const std::string &sourcefile = rawtokens.file(rawtok->location); + + const Token * const htok = rawtok->nextSkipComments(); + if (!sameline(rawtok, htok)) + continue; + + const bool systemheader = (htok->str()[0] == '<'); + const std::string header(htok->str().substr(1U, htok->str().size() - 2U)); + + const auto loadResult = cache.get(sourcefile, header, dui, systemheader, filenames, outputList); + const bool loaded = loadResult.second; + + if (!loaded) + continue; + + FileData *const filedata = loadResult.first; + + if (!filedata->tokens.front()) + continue; + + if (dui.removeComments) + filedata->tokens.removeComments(); + + filelist.emplace_back(filedata->tokens.front()); + } + + return cache; +} + +static bool preprocessToken(simplecpp::TokenList &output, const simplecpp::Token *&tok1, simplecpp::MacroMap ¯os, std::vector &files, simplecpp::OutputList *outputList) +{ + const simplecpp::Token * const tok = tok1; + const simplecpp::MacroMap::const_iterator it = tok->name ? macros.find(tok->str()) : macros.end(); + if (it != macros.end()) { + simplecpp::TokenList value(files); + try { + tok1 = it->second.expand(value, tok, macros, files); + } catch (const simplecpp::Macro::Error &err) { + if (outputList) { + simplecpp::Output out{ + simplecpp::Output::SYNTAX_ERROR, + err.location, + "failed to expand \'" + tok->str() + "\', " + err.what + }; + outputList->emplace_back(std::move(out)); + } + return false; + } + output.takeTokens(value); + } else { + if (!tok->comment) + output.push_back(new simplecpp::Token(*tok)); + tok1 = tok->next; + } + return true; +} + +static void getLocaltime(struct tm <ime) +{ + time_t t; + time(&t); +#ifndef _WIN32 + // NOLINTNEXTLINE(misc-include-cleaner) - false positive + localtime_r(&t, <ime); +#else + localtime_s(<ime, &t); +#endif +} + +static std::string getDateDefine(const struct tm *timep) +{ + char buf[] = "??? ?? ????"; + strftime(buf, sizeof(buf), "%b %d %Y", timep); + return std::string("\"").append(buf).append("\""); +} + +static std::string getTimeDefine(const struct tm *timep) +{ + char buf[] = "??:??:??"; + strftime(buf, sizeof(buf), "%H:%M:%S", timep); + return std::string("\"").append(buf).append("\""); +} + +void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenList &rawtokens, std::vector &files, simplecpp::FileDataCache &cache, const simplecpp::DUI &dui, simplecpp::OutputList *outputList, std::list *macroUsage, std::list *ifCond) +{ +#ifdef SIMPLECPP_WINDOWS + if (dui.clearIncludeCache) + nonExistingFilesCache.clear(); +#endif + + std::map sizeOfType(rawtokens.sizeOfType); + sizeOfType.insert(std::make_pair("char", sizeof(char))); + sizeOfType.insert(std::make_pair("short", sizeof(short))); + sizeOfType.insert(std::make_pair("short int", sizeOfType["short"])); + sizeOfType.insert(std::make_pair("int", sizeof(int))); + sizeOfType.insert(std::make_pair("long", sizeof(long))); + sizeOfType.insert(std::make_pair("long int", sizeOfType["long"])); + sizeOfType.insert(std::make_pair("long long", sizeof(long long))); + sizeOfType.insert(std::make_pair("float", sizeof(float))); + sizeOfType.insert(std::make_pair("double", sizeof(double))); + sizeOfType.insert(std::make_pair("long double", sizeof(long double))); + sizeOfType.insert(std::make_pair("char *", sizeof(char *))); + sizeOfType.insert(std::make_pair("short *", sizeof(short *))); + sizeOfType.insert(std::make_pair("short int *", sizeOfType["short *"])); + sizeOfType.insert(std::make_pair("int *", sizeof(int *))); + sizeOfType.insert(std::make_pair("long *", sizeof(long *))); + sizeOfType.insert(std::make_pair("long int *", sizeOfType["long *"])); + sizeOfType.insert(std::make_pair("long long *", sizeof(long long *))); + sizeOfType.insert(std::make_pair("float *", sizeof(float *))); + sizeOfType.insert(std::make_pair("double *", sizeof(double *))); + sizeOfType.insert(std::make_pair("long double *", sizeof(long double *))); + + // use a dummy vector for the macros because as this is not part of the file and would add an empty entry - e.g. /usr/include/poll.h + std::vector dummy; + + const bool hasInclude = isCpp17OrLater(dui) || isGnu(dui); + MacroMap macros; + bool strictAnsiDefined = false; + for (auto it = dui.defines.cbegin(); it != dui.defines.cend(); ++it) { + const std::string ¯ostr = *it; + const std::string::size_type eq = macrostr.find('='); + const std::string::size_type par = macrostr.find('('); + const std::string macroname = macrostr.substr(0, std::min(eq,par)); + if (macroname == "__STRICT_ANSI__") + strictAnsiDefined = true; + if (dui.undefined.find(macroname) != dui.undefined.end()) + continue; + const std::string lhs(macrostr.substr(0,eq)); + const std::string rhs(eq==std::string::npos ? std::string("1") : macrostr.substr(eq+1)); + try { + const Macro macro(lhs, rhs, dummy); + macros.insert(std::pair(macro.name(), macro)); + } catch (const std::runtime_error& e) { + if (outputList) { + simplecpp::Output err{ + Output::DUI_ERROR, + {}, + e.what() + }; + outputList->emplace_back(std::move(err)); + } + output.clear(); + return; + } + } + + const bool strictAnsiUndefined = dui.undefined.find("__STRICT_ANSI__") != dui.undefined.cend(); + if (!isGnu(dui) && !strictAnsiDefined && !strictAnsiUndefined) + macros.insert(std::pair("__STRICT_ANSI__", Macro("__STRICT_ANSI__", "1", dummy))); + + macros.insert(std::make_pair("__FILE__", Macro("__FILE__", "__FILE__", dummy))); + macros.insert(std::make_pair("__LINE__", Macro("__LINE__", "__LINE__", dummy))); + macros.insert(std::make_pair("__COUNTER__", Macro("__COUNTER__", "__COUNTER__", dummy))); + struct tm ltime {}; + getLocaltime(ltime); + macros.insert(std::make_pair("__DATE__", Macro("__DATE__", getDateDefine(<ime), dummy))); + macros.insert(std::make_pair("__TIME__", Macro("__TIME__", getTimeDefine(<ime), dummy))); + + if (!dui.std.empty()) { + const cstd_t c_std = simplecpp::getCStd(dui.std); + if (c_std != CUnknown) { + const std::string std_def = simplecpp::getCStdString(c_std); + if (!std_def.empty()) + macros.insert(std::make_pair("__STDC_VERSION__", Macro("__STDC_VERSION__", std_def, dummy))); + } else { + const cppstd_t cpp_std = simplecpp::getCppStd(dui.std); + if (cpp_std == CPPUnknown) { + if (outputList) { + simplecpp::Output err{ + Output::DUI_ERROR, + {}, + "unknown standard specified: '" + dui.std + "'" + }; + outputList->emplace_back(std::move(err)); + } + output.clear(); + return; + } + const std::string std_def = simplecpp::getCppStdString(cpp_std); + if (!std_def.empty()) + macros.insert(std::make_pair("__cplusplus", Macro("__cplusplus", std_def, dummy))); + } + } + + // True => code in current #if block should be kept + // ElseIsTrue => code in current #if block should be dropped. the code in the #else should be kept. + // AlwaysFalse => drop all code in #if and #else + enum IfState : std::uint8_t { True, ElseIsTrue, AlwaysFalse }; + std::stack ifstates; + std::stack iftokens; + ifstates.push(True); + + std::stack includetokenstack; + + std::set pragmaOnce; + + includetokenstack.push(rawtokens.cfront()); + for (auto it = dui.includes.cbegin(); it != dui.includes.cend(); ++it) { + const FileData *const filedata = cache.get("", *it, dui, false, files, outputList).first; + if (filedata != nullptr && filedata->tokens.cfront() != nullptr) + includetokenstack.push(filedata->tokens.cfront()); + } + + std::map> maybeUsedMacros; + + for (const Token *rawtok = nullptr; rawtok || !includetokenstack.empty();) { + if (rawtok == nullptr) { + rawtok = includetokenstack.top(); + includetokenstack.pop(); + continue; + } + + if (rawtok->op == '#' && !sameline(rawtok->previousSkipComments(), rawtok)) { + if (!sameline(rawtok, rawtok->next)) { + rawtok = rawtok->next; + continue; + } + rawtok = rawtok->next; + if (!rawtok->name) { + rawtok = gotoNextLine(rawtok); + continue; + } + + if (ifstates.size() <= 1U && (rawtok->str() == ELIF || rawtok->str() == ELSE || rawtok->str() == ENDIF)) { + if (outputList) { + simplecpp::Output err{ + Output::SYNTAX_ERROR, + rawtok->location, + "#" + rawtok->str() + " without #if" + }; + outputList->emplace_back(std::move(err)); + } + output.clear(); + return; + } + + if (ifstates.top() == True && (rawtok->str() == ERROR || rawtok->str() == WARNING)) { + if (outputList) { + std::string msg; + for (const Token *tok = rawtok->next; tok && sameline(rawtok,tok); tok = tok->next) { + if (!msg.empty() && isNameChar(tok->str()[0])) + msg += ' '; + msg += tok->str(); + } + msg = '#' + rawtok->str() + ' ' + msg; + simplecpp::Output err{ + rawtok->str() == ERROR ? Output::ERROR : Output::WARNING, + rawtok->location, + std::move(msg) + }; + + outputList->emplace_back(std::move(err)); + } + if (rawtok->str() == ERROR) { + output.clear(); + return; + } + } + + if (rawtok->str() == DEFINE) { + if (ifstates.top() != True) + continue; + try { + const Macro ¯o = Macro(rawtok->previous, files); + if (dui.undefined.find(macro.name()) == dui.undefined.end()) { + const MacroMap::iterator it = macros.find(macro.name()); + if (it == macros.end()) + macros.insert(std::pair(macro.name(), macro)); + else + it->second = macro; + } + } catch (const std::runtime_error &) { + if (outputList) { + simplecpp::Output err{ + Output::SYNTAX_ERROR, + rawtok->location, + "Failed to parse #define" + }; + outputList->emplace_back(std::move(err)); + } + output.clear(); + return; + } catch (const simplecpp::Macro::Error &err) { + if (outputList) { + simplecpp::Output out{ + simplecpp::Output::SYNTAX_ERROR, + err.location, + "Failed to parse #define, " + err.what + }; + outputList->emplace_back(std::move(out)); + } + output.clear(); + return; + } + } else if (ifstates.top() == True && rawtok->str() == INCLUDE) { + TokenList inc1(files); + for (const Token *inctok = rawtok->next; sameline(rawtok,inctok); inctok = inctok->next) { + if (!inctok->comment) + inc1.push_back(new Token(*inctok)); + } + TokenList inc2(files); + if (!inc1.empty() && inc1.cfront()->name) { + const Token *inctok = inc1.cfront(); + if (!preprocessToken(inc2, inctok, macros, files, outputList)) { + output.clear(); + return; + } + } else { + inc2.takeTokens(inc1); + } + + if (!inc1.empty() && !inc2.empty() && inc2.cfront()->op == '<' && inc2.cback()->op == '>') { + TokenString hdr; + // TODO: Sometimes spaces must be added in the string + // Somehow preprocessToken etc must be told that the location should be source location not destination location + for (const Token *tok = inc2.cfront(); tok; tok = tok->next) { + hdr += tok->str(); + } + inc2.clear(); + inc2.push_back(new Token(hdr, inc1.cfront()->location)); + inc2.front()->op = '<'; + } + + if (inc2.empty() || inc2.cfront()->str().size() <= 2U) { + if (outputList) { + simplecpp::Output err{ + Output::SYNTAX_ERROR, + rawtok->location, + "No header in #include" + }; + outputList->emplace_back(std::move(err)); + } + output.clear(); + return; + } + + const Token * const inctok = inc2.cfront(); + + const bool systemheader = (inctok->str()[0] == '<'); + const std::string header(inctok->str().substr(1U, inctok->str().size() - 2U)); + const FileData *const filedata = cache.get(rawtokens.file(rawtok->location), header, dui, systemheader, files, outputList).first; + if (filedata == nullptr) { + if (outputList) { + simplecpp::Output out{ + simplecpp::Output::MISSING_HEADER, + rawtok->location, + "Header not found: " + inctok->str() + }; + outputList->emplace_back(std::move(out)); + } + } else if (includetokenstack.size() >= 400) { + if (outputList) { + simplecpp::Output out{ + simplecpp::Output::INCLUDE_NESTED_TOO_DEEPLY, + rawtok->location, + "#include nested too deeply" + }; + outputList->emplace_back(std::move(out)); + } + } else if (pragmaOnce.find(filedata->filename) == pragmaOnce.end()) { + includetokenstack.push(gotoNextLine(rawtok)); + rawtok = filedata->tokens.cfront(); + continue; + } + } else if (rawtok->str() == IF || rawtok->str() == IFDEF || rawtok->str() == IFNDEF || rawtok->str() == ELIF) { + if (!sameline(rawtok,rawtok->next)) { + if (outputList) { + simplecpp::Output out{ + simplecpp::Output::SYNTAX_ERROR, + rawtok->location, + "Syntax error in #" + rawtok->str() + }; + outputList->emplace_back(std::move(out)); + } + output.clear(); + return; + } + + bool conditionIsTrue; + if (ifstates.top() == AlwaysFalse || (ifstates.top() == ElseIsTrue && rawtok->str() != ELIF)) { + conditionIsTrue = false; + } + else if (rawtok->str() == IFDEF) { + conditionIsTrue = (macros.find(rawtok->next->str()) != macros.end() || (hasInclude && rawtok->next->str() == HAS_INCLUDE)); + maybeUsedMacros[rawtok->next->str()].emplace_back(rawtok->next->location); + } else if (rawtok->str() == IFNDEF) { + conditionIsTrue = (macros.find(rawtok->next->str()) == macros.end() && !(hasInclude && rawtok->next->str() == HAS_INCLUDE)); + maybeUsedMacros[rawtok->next->str()].emplace_back(rawtok->next->location); + } else { /*if (rawtok->str() == IF || rawtok->str() == ELIF)*/ + TokenList expr(files); + for (const Token *tok = rawtok->next; tok && tok->location.sameline(rawtok->location); tok = tok->next) { + if (!tok->name) { + expr.push_back(new Token(*tok)); + continue; + } + + if (tok->str() == DEFINED) { + tok = tok->next; + const bool par = (tok && tok->op == '('); + if (par) + tok = tok->next; + maybeUsedMacros[rawtok->next->str()].emplace_back(rawtok->next->location); + if (tok) { + if (macros.find(tok->str()) != macros.end()) + expr.push_back(new Token("1", tok->location)); + else if (hasInclude && tok->str() == HAS_INCLUDE) + expr.push_back(new Token("1", tok->location)); + else + expr.push_back(new Token("0", tok->location)); + } + if (par) + tok = tok ? tok->next : nullptr; + if (!tok || !sameline(rawtok,tok) || (par && tok->op != ')')) { + if (outputList) { + Output out{ + Output::SYNTAX_ERROR, + rawtok->location, + "failed to evaluate " + std::string(rawtok->str() == IF ? "#if" : "#elif") + " condition" + }; + outputList->emplace_back(std::move(out)); + } + output.clear(); + return; + } + continue; + } + + if (hasInclude && tok->str() == HAS_INCLUDE) { + tok = tok->next; + const bool par = (tok && tok->op == '('); + if (par) + tok = tok->next; + bool closingAngularBracket = false; + if (tok) { + const std::string &sourcefile = rawtokens.file(rawtok->location); + const bool systemheader = (tok && tok->op == '<'); + std::string header; + + if (systemheader) { + while ((tok = tok->next) && tok->op != '>') + header += tok->str(); + if (tok && tok->op == '>') + closingAngularBracket = true; + } else { + header = tok->str().substr(1U, tok->str().size() - 2U); + closingAngularBracket = true; + } + if (tok) { + std::ifstream f; + const std::string header2 = openHeader(f,dui,sourcefile,header,systemheader); + expr.push_back(new Token(header2.empty() ? "0" : "1", tok->location)); + } + } + if (par) + tok = tok ? tok->next : nullptr; + if (!tok || !sameline(rawtok,tok) || (par && tok->op != ')') || (!closingAngularBracket)) { + if (outputList) { + Output out{ + Output::SYNTAX_ERROR, + rawtok->location, + "failed to evaluate " + std::string(rawtok->str() == IF ? "#if" : "#elif") + " condition" + }; + outputList->emplace_back(std::move(out)); + } + output.clear(); + return; + } + continue; + } + + maybeUsedMacros[rawtok->next->str()].emplace_back(rawtok->next->location); + + const Token *tmp = tok; + if (!preprocessToken(expr, tmp, macros, files, outputList)) { + output.clear(); + return; + } + if (!tmp) + break; + tok = tmp->previous; + } + try { + if (ifCond) { + std::string E; + for (const simplecpp::Token *tok = expr.cfront(); tok; tok = tok->next) + E += (E.empty() ? "" : " ") + tok->str(); + const long long result = evaluate(expr, dui, sizeOfType); + conditionIsTrue = (result != 0); + ifCond->emplace_back(rawtok->location, E, result); + } else { + const long long result = evaluate(expr, dui, sizeOfType); + conditionIsTrue = (result != 0); + } + } catch (const std::runtime_error &e) { + if (outputList) { + std::string msg = "failed to evaluate " + std::string(rawtok->str() == IF ? "#if" : "#elif") + " condition"; + if (e.what() && *e.what()) + msg += std::string(", ") + e.what(); + Output out{ + Output::SYNTAX_ERROR, + rawtok->location, + std::move(msg) + }; + outputList->emplace_back(std::move(out)); + } + output.clear(); + return; + } + } + + if (rawtok->str() != ELIF) { + // push a new ifstate.. + if (ifstates.top() != True) + ifstates.push(AlwaysFalse); + else + ifstates.push(conditionIsTrue ? True : ElseIsTrue); + iftokens.push(rawtok); + } else { + if (ifstates.top() == True) + ifstates.top() = AlwaysFalse; + else if (ifstates.top() == ElseIsTrue && conditionIsTrue) + ifstates.top() = True; + iftokens.top()->nextcond = rawtok; + iftokens.top() = rawtok; + } + } else if (rawtok->str() == ELSE) { + ifstates.top() = (ifstates.top() == ElseIsTrue) ? True : AlwaysFalse; + iftokens.top()->nextcond = rawtok; + iftokens.top() = rawtok; + } else if (rawtok->str() == ENDIF) { + ifstates.pop(); + iftokens.top()->nextcond = rawtok; + iftokens.pop(); + } else if (rawtok->str() == UNDEF) { + if (ifstates.top() == True) { + const Token *tok = rawtok->next; + while (sameline(rawtok,tok) && tok->comment) + tok = tok->next; + if (sameline(rawtok, tok)) + macros.erase(tok->str()); + } + } else if (ifstates.top() == True && rawtok->str() == PRAGMA && rawtok->next && rawtok->next->str() == ONCE && sameline(rawtok,rawtok->next)) { + pragmaOnce.insert(rawtokens.file(rawtok->location)); + } + if (ifstates.top() != True && rawtok->nextcond) + rawtok = rawtok->nextcond->previous; + else + rawtok = gotoNextLine(rawtok); + continue; + } + + if (ifstates.top() != True) { + // drop code + rawtok = gotoNextLine(rawtok); + continue; + } + + bool hash=false, hashhash=false; + if (rawtok->op == '#' && sameline(rawtok,rawtok->next)) { + if (rawtok->next->op != '#') { + hash = true; + rawtok = rawtok->next; // skip '#' + } else if (sameline(rawtok,rawtok->next->next)) { + hashhash = true; + rawtok = rawtok->next->next; // skip '#' '#' + } + } + + const Location loc(rawtok->location); + TokenList tokens(files); + + if (!preprocessToken(tokens, rawtok, macros, files, outputList)) { + output.clear(); + return; + } + + if (hash || hashhash) { + std::string s; + for (const Token *hashtok = tokens.cfront(); hashtok; hashtok = hashtok->next) + s += hashtok->str(); + if (hash) + output.push_back(new Token('\"' + s + '\"', loc)); + else if (output.back()) + output.back()->setstr(output.cback()->str() + s); + else + output.push_back(new Token(s, loc)); + } else { + output.takeTokens(tokens); + } + } + + if (macroUsage) { + for (simplecpp::MacroMap::const_iterator macroIt = macros.begin(); macroIt != macros.end(); ++macroIt) { + const Macro ¯o = macroIt->second; + std::list usage = macro.usage(); + const std::list& temp = maybeUsedMacros[macro.name()]; + usage.insert(usage.end(), temp.begin(), temp.end()); + for (std::list::const_iterator usageIt = usage.begin(); usageIt != usage.end(); ++usageIt) { + MacroUsage mu(macro.valueDefinedInCode()); + mu.macroName = macro.name(); + mu.macroLocation = macro.defineLocation(); + mu.useLocation = *usageIt; + macroUsage->emplace_back(std::move(mu)); + } + } + } +} + +void simplecpp::cleanup(FileDataCache &cache) +{ + cache.clear(); +} + +simplecpp::cstd_t simplecpp::getCStd(const std::string &std) +{ + if (std == "c90" || std == "c89" || std == "iso9899:1990" || std == "iso9899:199409" || std == "gnu90" || std == "gnu89") + return C89; + if (std == "c99" || std == "c9x" || std == "iso9899:1999" || std == "iso9899:199x" || std == "gnu99" || std == "gnu9x") + return C99; + if (std == "c11" || std == "c1x" || std == "iso9899:2011" || std == "gnu11" || std == "gnu1x") + return C11; + if (std == "c17" || std == "c18" || std == "iso9899:2017" || std == "iso9899:2018" || std == "gnu17" || std == "gnu18") + return C17; + if (std == "c23" || std == "gnu23" || std == "c2x" || std == "gnu2x") + return C23; + if (std == "c2y" || std == "gnu2y") + return C2Y; + return CUnknown; +} + +std::string simplecpp::getCStdString(cstd_t std) +{ + switch (std) { + case C89: + // __STDC_VERSION__ is not set for C90 although the macro was added in the 1994 amendments + return ""; + case C99: + return "199901L"; + case C11: + return "201112L"; + case C17: + return "201710L"; + case C23: + // supported by GCC 9+ and Clang 9+ + // Clang 9, 10, 11, 12, 13 return "201710L" + // Clang 14, 15, 16, 17 return "202000L" + // Clang 9, 10, 11, 12, 13, 14, 15, 16, 17 do not support "c23" and "gnu23" + return "202311L"; + case C2Y: + // supported by GCC 15+ and Clang 19+ + // Clang 19, 20, 21, 22 return "202400L" + return "202500L"; + case CUnknown: + return ""; + } + return ""; +} + +std::string simplecpp::getCStdString(const std::string &std) +{ + return getCStdString(getCStd(std)); +} + +simplecpp::cppstd_t simplecpp::getCppStd(const std::string &std) +{ + if (std == "c++98" || std == "c++03" || std == "gnu++98" || std == "gnu++03") + return CPP03; + if (std == "c++11" || std == "gnu++11" || std == "c++0x" || std == "gnu++0x") + return CPP11; + if (std == "c++14" || std == "c++1y" || std == "gnu++14" || std == "gnu++1y") + return CPP14; + if (std == "c++17" || std == "c++1z" || std == "gnu++17" || std == "gnu++1z") + return CPP17; + if (std == "c++20" || std == "c++2a" || std == "gnu++20" || std == "gnu++2a") + return CPP20; + if (std == "c++23" || std == "c++2b" || std == "gnu++23" || std == "gnu++2b") + return CPP23; + if (std == "c++26" || std == "c++2c" || std == "gnu++26" || std == "gnu++2c") + return CPP26; + return CPPUnknown; +} + +std::string simplecpp::getCppStdString(cppstd_t std) +{ + switch (std) { + case CPP03: + return "199711L"; + case CPP11: + return "201103L"; + case CPP14: + return "201402L"; + case CPP17: + return "201703L"; + case CPP20: + // GCC 10 returns "201703L" - correct in 11+ + return "202002L"; + case CPP23: + // supported by GCC 11+ and Clang 12+ + // GCC 11, 12, 13 return "202100L" + // Clang 12, 13, 14, 15, 16 do not support "c++23" and "gnu++23" and return "202101L" + // Clang 17, 18 return "202302L" + return "202302L"; + case CPP26: + // supported by GCC 14+ and Clang 17+ + return "202400L"; + case CPPUnknown: + return ""; + } + return ""; +} + +std::string simplecpp::getCppStdString(const std::string &std) +{ + return getCppStdString(getCppStd(std)); +} diff --git a/internal/cbm/vendored/simplecpp/simplecpp.h b/internal/cbm/vendored/simplecpp/simplecpp.h new file mode 100644 index 000000000..54f6a90c0 --- /dev/null +++ b/internal/cbm/vendored/simplecpp/simplecpp.h @@ -0,0 +1,636 @@ +/* -*- C++ -*- + * simplecpp - A simple and high-fidelity C/C++ preprocessor library + * Copyright (C) 2016-2023 simplecpp team + */ + +#ifndef simplecppH +#define simplecppH + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if __cplusplus >= 202002L +# include +#endif + +#if defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) +#include +#endif +#ifdef __cpp_lib_span +#include +#endif + +#ifdef _WIN32 +# ifdef SIMPLECPP_EXPORT +# define SIMPLECPP_LIB __declspec(dllexport) +# elif defined(SIMPLECPP_IMPORT) +# define SIMPLECPP_LIB __declspec(dllimport) +# else +# define SIMPLECPP_LIB +# endif +#else +# define SIMPLECPP_LIB +#endif + +#ifndef _WIN32 +# include +#endif + +#if defined(_MSC_VER) +# pragma warning(push) +// suppress warnings about "conversion from 'type1' to 'type2', possible loss of data" +# pragma warning(disable : 4267) +# pragma warning(disable : 4244) +#endif + +// provide legacy (i.e. raw pointer) API for TokenList +// note: std::istream has an overhead compared to raw pointers +#ifndef SIMPLECPP_TOKENLIST_ALLOW_PTR +// still provide the legacy API in case we lack the performant wrappers +# if !defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) +# define SIMPLECPP_TOKENLIST_ALLOW_PTR 1 +# else +# define SIMPLECPP_TOKENLIST_ALLOW_PTR 0 +# endif +#endif + +namespace simplecpp { + /** C code standard */ + enum cstd_t : std::int8_t { CUnknown=-1, C89, C99, C11, C17, C23, C2Y }; + + /** C++ code standard */ + enum cppstd_t : std::int8_t { CPPUnknown=-1, CPP03, CPP11, CPP14, CPP17, CPP20, CPP23, CPP26 }; + + using TokenString = std::string; + +#if defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) + using View = std::string_view; +#else + struct View + { + // cppcheck-suppress noExplicitConstructor + View(const char* data) + : mData(data) + , mSize(strlen(data)) + {} + + // only provide when std::span is not available so using untyped initilization won't use View +#if !defined(__cpp_lib_span) + View(const char* data, std::size_t size) + : mData(data) + , mSize(size) + {} + + // cppcheck-suppress noExplicitConstructor + View(const std::string& str) + : mData(str.data()) + , mSize(str.size()) + {} +#endif // !defined(__cpp_lib_span) + + const char* data() const { + return mData; + } + + std::size_t size() const { + return mSize; + } + + private: + const char* mData; + std::size_t mSize; + }; +#endif // defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) + + class Macro; + + /** + * Location in source code + */ + struct SIMPLECPP_LIB Location { + Location() = default; + Location(unsigned int fileIndex, unsigned int line, unsigned int col) + : fileIndex(fileIndex) + , line(line) + , col(col) + {} + + Location(const Location &loc) = default; + Location &operator=(const Location &other) = default; + + /** increment this location by string */ + void adjust(const std::string &str); + + bool operator<(const Location &rhs) const { + if (fileIndex != rhs.fileIndex) + return fileIndex < rhs.fileIndex; + if (line != rhs.line) + return line < rhs.line; + return col < rhs.col; + } + + bool sameline(const Location &other) const { + return fileIndex == other.fileIndex && line == other.line; + } + + unsigned int fileIndex{}; + unsigned int line{}; + unsigned int col{}; + }; + + /** + * token class. + * @todo don't use std::string representation - for both memory and performance reasons + */ + class SIMPLECPP_LIB Token { + public: + Token(const TokenString &s, const Location &loc, bool wsahead = false) : + whitespaceahead(wsahead), location(loc), string(s) { + flags(); + } + + Token(const Token &tok) : + macro(tok.macro), op(tok.op), comment(tok.comment), name(tok.name), number(tok.number), whitespaceahead(tok.whitespaceahead), location(tok.location), string(tok.string), mExpandedFrom(tok.mExpandedFrom) {} + + Token &operator=(const Token &tok) = delete; + + const TokenString& str() const { + return string; + } + void setstr(const std::string &s) { + string = s; + flags(); + } + + bool isOneOf(const char ops[]) const; + bool startsWithOneOf(const char c[]) const; + bool endsWithOneOf(const char c[]) const; + static bool isNumberLike(const std::string& str) { + return std::isdigit(static_cast(str[0])) || + (str.size() > 1U && (str[0] == '-' || str[0] == '+') && std::isdigit(static_cast(str[1]))); + } + + TokenString macro; + char op; + bool comment; + bool name; + bool number; + bool whitespaceahead; + Location location; + Token *previous{}; + Token *next{}; + mutable const Token *nextcond{}; + + const Token *previousSkipComments() const { + const Token *tok = this->previous; + while (tok && tok->comment) + tok = tok->previous; + return tok; + } + + const Token *nextSkipComments() const { + const Token *tok = this->next; + while (tok && tok->comment) + tok = tok->next; + return tok; + } + + void setExpandedFrom(const Token *tok, const Macro* m) { + mExpandedFrom = tok->mExpandedFrom; + mExpandedFrom.insert(m); + if (tok->whitespaceahead) + whitespaceahead = true; + } + bool isExpandedFrom(const Macro* m) const { + return mExpandedFrom.find(m) != mExpandedFrom.end(); + } + + void printAll() const; + void printOut() const; + private: + void flags() { + name = (std::isalpha(static_cast(string[0])) || string[0] == '_' || string[0] == '$') + && (std::memchr(string.c_str(), '\'', string.size()) == nullptr); + comment = string.size() > 1U && string[0] == '/' && (string[1] == '/' || string[1] == '*'); + number = isNumberLike(string); + op = (string.size() == 1U && !name && !comment && !number) ? string[0] : '\0'; + } + + TokenString string; + + std::set mExpandedFrom; + }; + + /** Output from preprocessor */ + struct SIMPLECPP_LIB Output { + enum Type : std::uint8_t { + ERROR, /* #error */ + WARNING, /* #warning */ + MISSING_HEADER, + INCLUDE_NESTED_TOO_DEEPLY, + SYNTAX_ERROR, + PORTABILITY_BACKSLASH, + UNHANDLED_CHAR_ERROR, + EXPLICIT_INCLUDE_NOT_FOUND, + FILE_NOT_FOUND, + DUI_ERROR + } type; + Output(Type type, const Location& loc, std::string msg) : type(type), location(loc), msg(std::move(msg)) {} + Location location; + std::string msg; + }; + + using OutputList = std::list; + + /** List of tokens. */ + class SIMPLECPP_LIB TokenList { + public: + class Stream; + + explicit TokenList(std::vector &filenames); + /** generates a token list from the given std::istream parameter */ + TokenList(std::istream &istr, std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr); + /** generates a token list from the given buffer */ + template + TokenList(const char (&data)[size], std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr) + : TokenList(reinterpret_cast(data), size-1, filenames, filename, outputList, 0) + {} + /** generates a token list from the given buffer */ + template + TokenList(const unsigned char (&data)[size], std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr) + : TokenList(data, size-1, filenames, filename, outputList, 0) + {} +#if SIMPLECPP_TOKENLIST_ALLOW_PTR + /** generates a token list from the given buffer */ + TokenList(const unsigned char* data, std::size_t size, std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr) + : TokenList(data, size, filenames, filename, outputList, 0) + {} + /** generates a token list from the given buffer */ + TokenList(const char* data, std::size_t size, std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr) + : TokenList(reinterpret_cast(data), size, filenames, filename, outputList, 0) + {} +#endif // SIMPLECPP_TOKENLIST_ALLOW_PTR + /** generates a token list from the given buffer */ + TokenList(View data, std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr) + : TokenList(reinterpret_cast(data.data()), data.size(), filenames, filename, outputList, 0) + {} +#ifdef __cpp_lib_span + /** generates a token list from the given buffer */ + TokenList(std::span data, std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr) + : TokenList(reinterpret_cast(data.data()), data.size(), filenames, filename, outputList, 0) + {} + + /** generates a token list from the given buffer */ + TokenList(std::span data, std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr) + : TokenList(data.data(), data.size(), filenames, filename, outputList, 0) + {} +#endif // __cpp_lib_span + + /** generates a token list from the given filename parameter */ + TokenList(const std::string &filename, std::vector &filenames, OutputList *outputList = nullptr); + TokenList(const TokenList &other); + TokenList(TokenList &&other); + ~TokenList(); + TokenList &operator=(const TokenList &other); + TokenList &operator=(TokenList &&other); + + void clear(); + bool empty() const { + return !frontToken; + } + void push_back(Token *tok); + + void dump(bool linenrs = false) const; + std::string stringify(bool linenrs = false) const; + + void readfile(Stream &stream, const std::string &filename=std::string(), OutputList *outputList = nullptr); + /** + * @throws std::overflow_error thrown on overflow or division by zero + * @throws std::runtime_error thrown on invalid expressions + */ + void constFold(); + + void removeComments(); + + Token *front() { + return frontToken; + } + + const Token *cfront() const { + return frontToken; + } + + Token *back() { + return backToken; + } + + const Token *cback() const { + return backToken; + } + + void deleteToken(Token *tok) { + if (!tok) + return; + Token * const prev = tok->previous; + Token * const next = tok->next; + if (prev) + prev->next = next; + if (next) + next->previous = prev; + if (frontToken == tok) + frontToken = next; + if (backToken == tok) + backToken = prev; + delete tok; + } + + void takeTokens(TokenList &other) { + if (!other.frontToken) + return; + if (!frontToken) { + frontToken = other.frontToken; + } else { + backToken->next = other.frontToken; + other.frontToken->previous = backToken; + } + backToken = other.backToken; + other.frontToken = other.backToken = nullptr; + } + + /** sizeof(T) */ + std::map sizeOfType; + + const std::vector& getFiles() const { + return files; + } + + const std::string& file(const Location& loc) const; + + private: + TokenList(const unsigned char* data, std::size_t size, std::vector &filenames, const std::string &filename, OutputList *outputList, int unused); + + void combineOperators(); + + void constFoldUnaryNotPosNeg(Token *tok); + /** + * @throws std::overflow_error thrown on overflow or division by zero + */ + void constFoldMulDivRem(Token *tok); + void constFoldAddSub(Token *tok); + void constFoldShift(Token *tok); + void constFoldComparison(Token *tok); + void constFoldBitwise(Token *tok); + void constFoldLogicalOp(Token *tok); + /** + * @throws std::runtime_error thrown on invalid expressions + */ + void constFoldQuestionOp(Token *&tok1); + + std::string readUntil(Stream &stream, const Location &location, char start, char end, OutputList *outputList); + void lineDirective(unsigned int fileIndex, unsigned int line, Location &location); + + const Token* lastLineTok(int maxsize=1000) const; + const Token* isLastLinePreprocessor(int maxsize=1000) const; + + unsigned int fileIndex(const std::string &filename); + + Token *frontToken; + Token *backToken; + std::vector &files; + }; + + /** Tracking how macros are used */ + struct SIMPLECPP_LIB MacroUsage { + explicit MacroUsage(bool macroValueKnown_) : macroValueKnown(macroValueKnown_) {} + std::string macroName; + Location macroLocation; + Location useLocation; + bool macroValueKnown; + }; + + /** Tracking #if/#elif expressions */ + struct SIMPLECPP_LIB IfCond { + explicit IfCond(const Location& location, const std::string &E, long long result) : location(location), E(E), result(result) {} + Location location; // location of #if/#elif + std::string E; // preprocessed condition + long long result; // condition result + }; + + /** + * Command line preprocessor settings. + * On the command line these are configured by -D, -U, -I, --include, -std + */ + struct SIMPLECPP_LIB DUI { + DUI() = default; + std::list defines; + std::set undefined; + std::list includePaths; + std::list includes; + std::string std; + bool clearIncludeCache{}; + bool removeComments{}; /** remove comment tokens from included files */ + }; + + struct SIMPLECPP_LIB FileData { + /** The canonical filename associated with this data */ + std::string filename; + /** The tokens associated with this file */ + TokenList tokens; + }; + + class SIMPLECPP_LIB FileDataCache { + public: + FileDataCache() = default; + + FileDataCache(const FileDataCache &) = delete; + FileDataCache(FileDataCache &&) = default; + + FileDataCache &operator=(const FileDataCache &) = delete; + FileDataCache &operator=(FileDataCache &&) = default; + + /** Get the cached data for a file, or load and then return it if it isn't cached. + * returns the file data and true if the file was loaded, false if it was cached. */ + std::pair get(const std::string &sourcefile, const std::string &header, const DUI &dui, bool systemheader, std::vector &filenames, OutputList *outputList); + + void insert(FileData data) { + // NOLINTNEXTLINE(misc-const-correctness) - FP + auto *const newdata = new FileData(std::move(data)); + + mData.emplace_back(newdata); + mNameMap.emplace(newdata->filename, newdata); + } + + void clear() { + mNameMap.clear(); + mIdMap.clear(); + mData.clear(); + } + + using container_type = std::vector>; + using iterator = container_type::iterator; + using const_iterator = container_type::const_iterator; + using size_type = container_type::size_type; + + size_type size() const { + return mData.size(); + } + iterator begin() { + return mData.begin(); + } + iterator end() { + return mData.end(); + } + const_iterator begin() const { + return mData.begin(); + } + const_iterator end() const { + return mData.end(); + } + const_iterator cbegin() const { + return mData.cbegin(); + } + const_iterator cend() const { + return mData.cend(); + } + + private: + struct FileID { +#ifdef _WIN32 + struct { + std::uint64_t VolumeSerialNumber; + struct { + std::uint64_t IdentifierHi; + std::uint64_t IdentifierLo; + } FileId; + } fileIdInfo; + + bool operator==(const FileID &that) const noexcept { + return fileIdInfo.VolumeSerialNumber == that.fileIdInfo.VolumeSerialNumber && + fileIdInfo.FileId.IdentifierHi == that.fileIdInfo.FileId.IdentifierHi && + fileIdInfo.FileId.IdentifierLo == that.fileIdInfo.FileId.IdentifierLo; + } +#else + dev_t dev; + ino_t ino; + + bool operator==(const FileID& that) const noexcept { + return dev == that.dev && ino == that.ino; + } +#endif + struct Hasher { + std::size_t operator()(const FileID &id) const { +#ifdef _WIN32 + return static_cast(id.fileIdInfo.FileId.IdentifierHi ^ id.fileIdInfo.FileId.IdentifierLo ^ + id.fileIdInfo.VolumeSerialNumber); +#else + return static_cast(id.dev) ^ static_cast(id.ino); +#endif + } + }; + }; + + using name_map_type = std::unordered_map; + using id_map_type = std::unordered_map; + + static bool getFileId(const std::string &path, FileID &id); + + std::pair tryload(name_map_type::iterator &name_it, const DUI &dui, std::vector &filenames, OutputList *outputList); + + container_type mData; + name_map_type mNameMap; + id_map_type mIdMap; + }; + + /** Converts character literal (including prefix, but not ud-suffix) to long long value. + * + * Assumes ASCII-compatible single-byte encoded str for narrow literals + * and UTF-8 otherwise. + * + * For target assumes + * - execution character set encoding matching str + * - UTF-32 execution wide-character set encoding + * - requirements for __STDC_UTF_16__, __STDC_UTF_32__ and __STDC_ISO_10646__ satisfied + * - char16_t is 16bit wide + * - char32_t is 32bit wide + * - wchar_t is 32bit wide and unsigned + * - matching char signedness to host + * - matching sizeof(int) to host + * + * For host assumes + * - ASCII-compatible execution character set + * + * For host and target assumes + * - CHAR_BIT == 8 + * - two's complement + * + * Implements multi-character narrow literals according to GCC's behavior, + * except multi code unit universal character names are not supported. + * Multi-character wide literals are not supported. + * Limited support of universal character names for non-UTF-8 execution character set encodings. + * @throws std::runtime_error thrown on invalid literal + */ + SIMPLECPP_LIB long long characterLiteralToLL(const std::string& str); + + SIMPLECPP_LIB FileDataCache load(const TokenList &rawtokens, std::vector &filenames, const DUI &dui, OutputList *outputList = nullptr, FileDataCache cache = {}); + + /** + * Preprocess + * @todo simplify interface + * @param output TokenList that receives the preprocessing output + * @param rawtokens Raw tokenlist for top sourcefile + * @param files internal data of simplecpp + * @param cache output from simplecpp::load() + * @param dui defines, undefs, and include paths + * @param outputList output: list that will receive output messages + * @param macroUsage output: macro usage + * @param ifCond output: #if/#elif expressions + */ + SIMPLECPP_LIB void preprocess(TokenList &output, const TokenList &rawtokens, std::vector &files, FileDataCache &cache, const DUI &dui, OutputList *outputList = nullptr, std::list *macroUsage = nullptr, std::list *ifCond = nullptr); + + /** + * Deallocate data + */ + SIMPLECPP_LIB void cleanup(FileDataCache &cache); + + /** Simplify path */ + SIMPLECPP_LIB std::string simplifyPath(std::string path); + + /** Convert Cygwin path to Windows path */ + SIMPLECPP_LIB std::string convertCygwinToWindowsPath(const std::string &cygwinPath); + + /** Returns the C version a given standard */ + SIMPLECPP_LIB cstd_t getCStd(const std::string &std); + + /** Returns the C++ version a given standard */ + SIMPLECPP_LIB cppstd_t getCppStd(const std::string &std); + + /** Returns the __STDC_VERSION__ value for a given standard */ + SIMPLECPP_LIB std::string getCStdString(const std::string &std); + SIMPLECPP_LIB std::string getCStdString(cstd_t std); + + /** Returns the __cplusplus value for a given standard */ + SIMPLECPP_LIB std::string getCppStdString(const std::string &std); + SIMPLECPP_LIB std::string getCppStdString(cppstd_t std); + + /** Checks if given path is absolute */ + SIMPLECPP_LIB bool isAbsolutePath(const std::string &path); +} + +#undef SIMPLECPP_TOKENLIST_ALLOW_PTR + +#if defined(_MSC_VER) +# pragma warning(pop) +#endif + +#undef SIMPLECPP_LIB + +#endif diff --git a/internal/discover/discover.go b/internal/discover/discover.go index 8062d9fd9..a02500b6a 100644 --- a/internal/discover/discover.go +++ b/internal/discover/discover.go @@ -163,6 +163,24 @@ var fastIgnorePatterns = []string{ ".test.", // test files } +// isWorktreeDir reports whether dir is a git worktree or submodule checkout. +// These contain a .git *file* (not directory) whose first line starts with "gitdir:". +func isWorktreeDir(dir string) bool { + gitPath := filepath.Join(dir, ".git") + fi, err := os.Lstat(gitPath) + if err != nil || fi.IsDir() { + return false + } + f, err := os.Open(gitPath) + if err != nil { + return false + } + defer f.Close() + buf := make([]byte, 7) + n, _ := f.Read(buf) + return n == 7 && string(buf) == "gitdir:" +} + // shouldSkipDir returns true if the directory should be skipped during discovery. func shouldSkipDir(name, rel string, extraIgnore []string, mode IndexMode) bool { if IGNORE_PATTERNS[name] { @@ -364,6 +382,11 @@ func Discover(ctx context.Context, repoPath string, opts *Options) ([]FileInfo, if shouldSkipDir(info.Name(), rel, extraIgnore, mode) { return filepath.SkipDir } + // Detect worktree/submodule checkouts (.git file with "gitdir:" pointer) + if rel != "." && isWorktreeDir(path) { + slog.Debug("discover.skip_worktree", "dir", rel) + return filepath.SkipDir + } // Then gitignore + cbmignore (skip root — library panics on base==path) if rel != "." && matchers.shouldIgnore(path, true) { slog.Debug("discover.gitignore_skip", "dir", rel) diff --git a/internal/discover/discover_test.go b/internal/discover/discover_test.go index 449713d88..bab05ee7a 100644 --- a/internal/discover/discover_test.go +++ b/internal/discover/discover_test.go @@ -108,6 +108,67 @@ func TestDiscoverSkipsWorktrees(t *testing.T) { } } +func TestDiscoverSkipsWorktreeCheckout(t *testing.T) { + dir := t.TempDir() + + if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0o600); err != nil { + t.Fatal(err) + } + + // Create a worktree-like directory with a .git file containing "gitdir:" + wtDir := filepath.Join(dir, "feature-branch") + if err := os.MkdirAll(filepath.Join(wtDir, "src"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(wtDir, "src", "lib.go"), []byte("package lib\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(wtDir, ".git"), []byte("gitdir: /repo/.git/worktrees/feature-branch\n"), 0o600); err != nil { + t.Fatal(err) + } + + files, err := Discover(context.Background(), dir, nil) + if err != nil { + t.Fatalf("Discover: %v", err) + } + + if len(files) != 1 { + t.Fatalf("expected 1 file (skipping worktree checkout), got %d", len(files)) + } + if filepath.Base(files[0].Path) != "main.go" { + t.Errorf("expected main.go, got %s", files[0].Path) + } +} + +func TestDiscoverDoesNotSkipDirWithNonGitdirDotGitFile(t *testing.T) { + dir := t.TempDir() + + if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0o600); err != nil { + t.Fatal(err) + } + + // Create a directory with a .git file that does NOT start with "gitdir:" + subDir := filepath.Join(dir, "subproject") + if err := os.MkdirAll(subDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(subDir, "lib.go"), []byte("package lib\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(subDir, ".git"), []byte("not a gitdir pointer\n"), 0o600); err != nil { + t.Fatal(err) + } + + files, err := Discover(context.Background(), dir, nil) + if err != nil { + t.Fatalf("Discover: %v", err) + } + + if len(files) != 2 { + t.Fatalf("expected 2 files (subdir NOT skipped), got %d", len(files)) + } +} + func TestDiscoverCancellation(t *testing.T) { dir := t.TempDir() diff --git a/internal/pipeline/c_lsp_cross.go b/internal/pipeline/c_lsp_cross.go new file mode 100644 index 000000000..5a9cd7118 --- /dev/null +++ b/internal/pipeline/c_lsp_cross.go @@ -0,0 +1,251 @@ +package pipeline + +import ( + "log/slog" + "path/filepath" + "strings" + + "github.com/DeusData/codebase-memory-mcp/internal/cbm" + "github.com/DeusData/codebase-memory-mcp/internal/fqn" + "github.com/DeusData/codebase-memory-mcp/internal/lang" +) + +// cLSPDefIndex indexes cross-file definitions by header/source path for C/C++ LSP resolution. +type cLSPDefIndex struct { + // byRelPath maps file relative path → []CrossFileDef (for #include "local/path.h") + byRelPath map[string][]cbm.CrossFileDef + // byDir maps directory relative path → []CrossFileDef (for collecting all defs in a dir) + byDir map[string][]cbm.CrossFileDef + // includeGraph maps file relative path → resolved paths of directly included files + includeGraph map[string][]string +} + +// isCOrCPP returns true if the language is C, C++, or CUDA. +func isCOrCPP(l lang.Language) bool { + return l == lang.C || l == lang.CPP || l == lang.CUDA +} + +// resolveFileIncludes builds the list of resolved include paths for a file's imports. +func (p *Pipeline) resolveFileIncludes(imports []cbm.Import, currentDir string, incDirs []string) []string { + var includes []string + for _, imp := range imports { + if imp.ModulePath == "" { + continue + } + for _, candidate := range resolveIncludePath(imp.ModulePath, currentDir, incDirs) { + if _, exists := p.extractionCache[candidate]; exists { + includes = append(includes, candidate) + break + } + } + } + return includes +} + +// indexFileDefs adds a file's definitions to the index by path and directory. +func (idx *cLSPDefIndex) indexFileDefs(relPath string, defs []cbm.CrossFileDef) { + idx.byRelPath[relPath] = append(idx.byRelPath[relPath], defs...) + dir := filepath.Dir(relPath) + if dir == "." { + dir = "" + } + idx.byDir[dir] = append(idx.byDir[dir], defs...) +} + +// buildCLSPDefIndex builds the cross-file definition index for C/C++ LSP resolution. +// Scans all C/C++/CUDA files in extractionCache, indexes defs by file path and directory. +func (p *Pipeline) buildCLSPDefIndex() *cLSPDefIndex { + idx := &cLSPDefIndex{ + byRelPath: make(map[string][]cbm.CrossFileDef), + byDir: make(map[string][]cbm.CrossFileDef), + includeGraph: make(map[string][]string), + } + + allIncDirs := p.getAllRelativeIncludeDirs() + + hasCFiles := false + for relPath, ext := range p.extractionCache { + if !isCOrCPP(ext.Language) || ext.Result == nil { + continue + } + hasCFiles = true + + // Build include graph from imports + if len(ext.Result.Imports) > 0 { + incDirs := p.getRelativeIncludeDirs(relPath) + if incDirs == nil { + incDirs = allIncDirs + } + includes := p.resolveFileIncludes(ext.Result.Imports, filepath.Dir(relPath), incDirs) + if len(includes) > 0 { + idx.includeGraph[relPath] = includes + } + } + + if len(ext.Result.Definitions) == 0 { + continue + } + + defs := cbm.DefsToLSPDefs(ext.Result.Definitions, fqn.ModuleQN(p.ProjectName, relPath)) + if len(defs) > 0 { + idx.indexFileDefs(relPath, defs) + } + } + + if !hasCFiles { + return nil + } + + totalDefs := 0 + for _, defs := range idx.byRelPath { + totalDefs += len(defs) + } + if totalDefs > 0 { + slog.Info("c_lsp.cross_file.index", + "files", len(idx.byRelPath), + "defs", totalDefs, + "include_edges", len(idx.includeGraph), + ) + } + + return idx +} + +type bfsEntry struct { + path string + depth int +} + +// collectDefsForFile adds defs from a file and its corresponding source file. +func (idx *cLSPDefIndex) collectDefsForFile(filePath string, seen map[string]bool) []cbm.CrossFileDef { + var result []cbm.CrossFileDef + if defs, ok := idx.byRelPath[filePath]; ok { + result = append(result, defs...) + } + for _, srcExt := range correspondingSourceExts(filePath) { + srcPath := swapExtension(filePath, srcExt) + if seen[srcPath] { + continue + } + seen[srcPath] = true + if defs, ok := idx.byRelPath[srcPath]; ok { + result = append(result, defs...) + } + } + return result +} + +// seedDirectIncludes resolves direct imports and returns the initial BFS queue. +func (idx *cLSPDefIndex) seedDirectIncludes(imports []cbm.Import, currentDir string, + extraIncDirs []string, seen map[string]bool) ([]cbm.CrossFileDef, []bfsEntry) { + + var result []cbm.CrossFileDef + var queue []bfsEntry + + for _, imp := range imports { + if imp.ModulePath == "" { + continue + } + for _, candidate := range resolveIncludePath(imp.ModulePath, currentDir, extraIncDirs) { + if seen[candidate] { + continue + } + seen[candidate] = true + result = append(result, idx.collectDefsForFile(candidate, seen)...) + queue = append(queue, bfsEntry{candidate, 1}) + } + } + return result, queue +} + +// collectCrossFileDefs returns cross-file definitions for all transitively included files. +// Uses BFS on the include graph (max depth 8) to follow #include chains. +// Also includes defs from corresponding source files and same-directory files. +// extraIncDirs are additional include directories from compile_commands.json. +func (idx *cLSPDefIndex) collectCrossFileDefs(imports []cbm.Import, currentRelPath string, extraIncDirs []string) []cbm.CrossFileDef { + if idx == nil || len(imports) == 0 { + return nil + } + + seen := make(map[string]bool) + currentDir := filepath.Dir(currentRelPath) + + // Seed BFS queue with direct includes + result, queue := idx.seedDirectIncludes(imports, currentDir, extraIncDirs, seen) + + // BFS: follow transitive includes up to depth 8 + const maxDepth = 8 + for i := 0; i < len(queue); i++ { + entry := queue[i] + if entry.depth >= maxDepth { + continue + } + for _, child := range idx.includeGraph[entry.path] { + if seen[child] { + continue + } + seen[child] = true + result = append(result, idx.collectDefsForFile(child, seen)...) + queue = append(queue, bfsEntry{child, entry.depth + 1}) + } + } + + // Same-directory defs (common C/C++ pattern) + if !seen["__dir__"+currentDir] { + seen["__dir__"+currentDir] = true + if defs, ok := idx.byDir[currentDir]; ok { + for i := range defs { + if !seen["__def__"+defs[i].QualifiedName] { + seen["__def__"+defs[i].QualifiedName] = true + result = append(result, defs[i]) + } + } + } + } + + return result +} + +// resolveIncludePath returns candidate relative paths for a #include path. +// For #include "foo/bar.h", tries: relative to current dir, compile_commands include dirs, +// then as-is from repo root. +func resolveIncludePath(includePath, currentDir string, extraIncDirs []string) []string { + includePath = strings.Trim(includePath, "\"<>") + var candidates []string + + // Relative to current file's directory + if currentDir != "" && currentDir != "." { + candidates = append(candidates, filepath.Join(currentDir, includePath)) + } + + // Compile_commands.json include directories (already relative to repo root) + for _, dir := range extraIncDirs { + candidates = append(candidates, filepath.Join(dir, includePath)) + } + + // As-is from repo root + candidates = append(candidates, includePath) + + return candidates +} + +// correspondingSourceExts returns source file extensions to check for a header file. +func correspondingSourceExts(path string) []string { + ext := filepath.Ext(path) + switch ext { + case ".h": + return []string{".c", ".cpp", ".cc", ".cxx"} + case ".hpp", ".hxx", ".hh": + return []string{".cpp", ".cc", ".cxx"} + case ".cuh": // CUDA header + return []string{".cu"} + default: + return nil + } +} + +// swapExtension replaces the file extension. +func swapExtension(path, newExt string) string { + ext := filepath.Ext(path) + return path[:len(path)-len(ext)] + newExt +} diff --git a/internal/pipeline/compile_commands.go b/internal/pipeline/compile_commands.go new file mode 100644 index 000000000..8d51cc2aa --- /dev/null +++ b/internal/pipeline/compile_commands.go @@ -0,0 +1,256 @@ +package pipeline + +import ( + "encoding/json" + "log/slog" + "os" + "path/filepath" + "strings" + + "github.com/DeusData/codebase-memory-mcp/internal/cbm" +) + +// CompileFlags holds extracted compilation flags for a single file. +type CompileFlags struct { + IncludePaths []string // -I and -isystem paths (resolved to absolute) + Defines []string // -D defines as "NAME=VALUE" or "NAME" + Standard string // -std= value (e.g., "c++17", "c11") +} + +// CompileFlagsMap maps relative file paths to their compile flags. +type CompileFlagsMap map[string]*CompileFlags + +// compileCommandEntry is a single entry in compile_commands.json. +type compileCommandEntry struct { + Directory string `json:"directory"` + File string `json:"file"` + Command string `json:"command"` + Arguments []string `json:"arguments"` + Output string `json:"output"` +} + +// loadCompileCommands searches for compile_commands.json in common locations +// relative to repoPath, parses it, and returns per-file compile flags. +func loadCompileCommands(repoPath string) CompileFlagsMap { + candidates := []string{ + filepath.Join(repoPath, "compile_commands.json"), + filepath.Join(repoPath, "build", "compile_commands.json"), + filepath.Join(repoPath, "out", "compile_commands.json"), + } + + // Also check cmake-build-* directories + entries, _ := os.ReadDir(repoPath) + for _, e := range entries { + if e.IsDir() && strings.HasPrefix(e.Name(), "cmake-build-") { + candidates = append(candidates, + filepath.Join(repoPath, e.Name(), "compile_commands.json")) + } + } + + for _, path := range candidates { + data, err := os.ReadFile(path) + if err != nil { + continue + } + result := parseCompileCommands(data, repoPath) + if len(result) > 0 { + slog.Info("compile_commands.loaded", "path", path, "entries", len(result)) + return result + } + } + + return nil +} + +// parseCompileCommands parses compile_commands.json content and extracts +// per-file include paths, defines, and standard flags. +func parseCompileCommands(data []byte, repoPath string) CompileFlagsMap { + var entries []compileCommandEntry + if err := json.Unmarshal(data, &entries); err != nil { + slog.Warn("compile_commands.parse_err", "err", err) + return nil + } + + result := make(CompileFlagsMap, len(entries)) + for _, e := range entries { + // Get the arguments list + args := e.Arguments + if len(args) == 0 && e.Command != "" { + args = splitCommand(e.Command) + } + if len(args) == 0 { + continue + } + + flags := extractFlags(args, e.Directory) + + // Resolve file path to relative + filePath := e.File + if !filepath.IsAbs(filePath) && e.Directory != "" { + filePath = filepath.Join(e.Directory, filePath) + } + relPath, err := filepath.Rel(repoPath, filePath) + if err != nil || strings.HasPrefix(relPath, "..") { + continue + } + relPath = filepath.ToSlash(relPath) + + result[relPath] = flags + } + + return result +} + +// extractFlags extracts -I, -isystem, -D, and -std= flags from compiler arguments. +func extractFlags(args []string, directory string) *CompileFlags { + flags := &CompileFlags{} + + for i := 0; i < len(args); i++ { + arg := args[i] + + // Include paths: -I or -I + if strings.HasPrefix(arg, "-I") { + path := arg[2:] + if path == "" && i+1 < len(args) { + i++ + path = args[i] + } + if path != "" { + flags.IncludePaths = append(flags.IncludePaths, resolvePath(path, directory)) + } + continue + } + + // System include paths: -isystem + if arg == "-isystem" && i+1 < len(args) { + i++ + flags.IncludePaths = append(flags.IncludePaths, resolvePath(args[i], directory)) + continue + } + + // Defines: -D or -D= or -D + if strings.HasPrefix(arg, "-D") { + define := arg[2:] + if define == "" && i+1 < len(args) { + i++ + define = args[i] + } + if define != "" { + flags.Defines = append(flags.Defines, define) + } + continue + } + + // Standard: -std=c++17 etc. + if strings.HasPrefix(arg, "-std=") { + flags.Standard = arg[5:] + continue + } + } + + return flags +} + +// resolvePath resolves a path relative to a directory, returning absolute path. +func resolvePath(path, directory string) string { + if filepath.IsAbs(path) { + return filepath.Clean(path) + } + if directory != "" { + return filepath.Clean(filepath.Join(directory, path)) + } + return filepath.Clean(path) +} + +// getRelativeIncludeDirs returns include paths relative to repo root for a file. +// Used by cross-file include resolution (extractionCache uses repo-relative paths). +func (p *Pipeline) getRelativeIncludeDirs(relPath string) []string { + if p.compileFlags == nil { + return nil + } + flags, ok := p.compileFlags[relPath] + if !ok || len(flags.IncludePaths) == 0 { + return nil + } + var relDirs []string + for _, absPath := range flags.IncludePaths { + rel, err := filepath.Rel(p.RepoPath, absPath) + if err != nil || strings.HasPrefix(rel, "..") { + continue // outside repo — skip + } + relDirs = append(relDirs, filepath.ToSlash(rel)) + } + return relDirs +} + +// getAllRelativeIncludeDirs returns the union of all include dirs across all files. +// Used when resolving includes for files not in compile_commands. +func (p *Pipeline) getAllRelativeIncludeDirs() []string { + if p.compileFlags == nil { + return nil + } + seen := make(map[string]bool) + var dirs []string + for _, flags := range p.compileFlags { + for _, absPath := range flags.IncludePaths { + rel, err := filepath.Rel(p.RepoPath, absPath) + if err != nil || strings.HasPrefix(rel, "..") { + continue + } + relSlash := filepath.ToSlash(rel) + if !seen[relSlash] { + seen[relSlash] = true + dirs = append(dirs, relSlash) + } + } + } + return dirs +} + +// getCompileFlags returns ExtractionFlags for a file, or nil if no compile_commands data. +func (p *Pipeline) getCompileFlags(relPath string) *cbm.ExtractionFlags { + if p.compileFlags == nil { + return nil + } + flags, ok := p.compileFlags[relPath] + if !ok { + return nil + } + return &cbm.ExtractionFlags{ + IncludePaths: flags.IncludePaths, + Defines: flags.Defines, + } +} + +// splitCommand does a simple shell-like split of a command string. +// Handles basic quoting but not escapes. +func splitCommand(cmd string) []string { + var args []string + var current strings.Builder + inQuote := byte(0) + + for i := 0; i < len(cmd); i++ { + c := cmd[i] + switch { + case inQuote != 0: + if c == inQuote { + inQuote = 0 + } else { + current.WriteByte(c) + } + case c == '"' || c == '\'': + inQuote = c + case c == ' ' || c == '\t': + if current.Len() > 0 { + args = append(args, current.String()) + current.Reset() + } + default: + current.WriteByte(c) + } + } + if current.Len() > 0 { + args = append(args, current.String()) + } + return args +} diff --git a/internal/pipeline/compile_commands_test.go b/internal/pipeline/compile_commands_test.go new file mode 100644 index 000000000..80ea5d930 --- /dev/null +++ b/internal/pipeline/compile_commands_test.go @@ -0,0 +1,121 @@ +package pipeline + +import ( + "testing" +) + +func TestParseCompileCommands(t *testing.T) { + data := []byte(`[ + { + "directory": "/home/user/project/build", + "command": "gcc -I/home/user/project/include -I/home/user/project/src -DDEBUG=1 -DVERSION=\"1.0\" -std=c11 -o main.o -c /home/user/project/src/main.c", + "file": "/home/user/project/src/main.c" + }, + { + "directory": "/home/user/project/build", + "arguments": ["g++", "-I/home/user/project/include", "-isystem", "/home/user/project/third_party", "-DUSE_SSL", "-std=c++17", "-c", "/home/user/project/src/server.cpp"], + "file": "/home/user/project/src/server.cpp" + }, + { + "directory": "/home/user/project/build", + "command": "gcc -c /outside/repo/file.c", + "file": "/outside/repo/file.c" + } + ]`) + + repoPath := "/home/user/project" + result := parseCompileCommands(data, repoPath) + + // main.c + mainFlags := result["src/main.c"] + if mainFlags == nil { + t.Fatal("expected flags for src/main.c") + } + if len(mainFlags.IncludePaths) != 2 { + t.Errorf("expected 2 include paths for main.c, got %d: %v", len(mainFlags.IncludePaths), mainFlags.IncludePaths) + } + if len(mainFlags.Defines) != 2 { + t.Errorf("expected 2 defines for main.c, got %d: %v", len(mainFlags.Defines), mainFlags.Defines) + } + if mainFlags.Standard != "c11" { + t.Errorf("expected standard c11, got %q", mainFlags.Standard) + } + + // server.cpp (using arguments array) + serverFlags := result["src/server.cpp"] + if serverFlags == nil { + t.Fatal("expected flags for src/server.cpp") + } + if len(serverFlags.IncludePaths) != 2 { + t.Errorf("expected 2 include paths for server.cpp, got %d: %v", len(serverFlags.IncludePaths), serverFlags.IncludePaths) + } + if len(serverFlags.Defines) != 1 { + t.Errorf("expected 1 define for server.cpp, got %d: %v", len(serverFlags.Defines), serverFlags.Defines) + } + if serverFlags.Standard != "c++17" { + t.Errorf("expected standard c++17, got %q", serverFlags.Standard) + } + + // Outside-repo file should be excluded + if _, ok := result["../outside/repo/file.c"]; ok { + t.Error("should not include files outside repo") + } +} + +func TestParseCompileCommands_Empty(t *testing.T) { + result := parseCompileCommands([]byte(`[]`), "/repo") + if len(result) != 0 { + t.Errorf("expected empty result, got %d entries", len(result)) + } +} + +func TestParseCompileCommands_InvalidJSON(t *testing.T) { + result := parseCompileCommands([]byte(`not json`), "/repo") + if result != nil { + t.Error("expected nil for invalid JSON") + } +} + +func TestSplitCommand(t *testing.T) { + tests := []struct { + cmd string + want int + }{ + {"gcc -c main.c", 3}, + {`gcc -DFOO="bar baz" -c main.c`, 4}, + {"g++ -I/usr/include -std=c++17 -o out -c in.cpp", 7}, + } + + for _, tt := range tests { + got := splitCommand(tt.cmd) + if len(got) != tt.want { + t.Errorf("splitCommand(%q): got %d args %v, want %d", tt.cmd, len(got), got, tt.want) + } + } +} + +func TestExtractFlags(t *testing.T) { + args := []string{ + "g++", + "-I", "/abs/include", + "-I/rel/include", + "-isystem", "/sys/include", + "-DFOO", + "-DBAR=42", + "-std=c++20", + "-O2", + "-Wall", + "-c", "main.cpp", + } + + flags := extractFlags(args, "/project") + if len(flags.IncludePaths) != 3 { + t.Errorf("expected 3 include paths, got %d: %v", len(flags.IncludePaths), flags.IncludePaths) + } + if len(flags.Defines) != 2 { + t.Errorf("expected 2 defines, got %d: %v", len(flags.Defines), flags.Defines) + } + if flags.Standard != "c++20" { + t.Errorf("expected c++20, got %q", flags.Standard) + } +} diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 0e5efc2a1..26010abaa 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -47,6 +47,10 @@ type Pipeline struct { returnTypes ReturnTypeMap // goLSPIdx indexes Go cross-file definitions for LSP resolution in pass3 goLSPIdx *goLSPDefIndex + // cLSPIdx indexes C/C++/CUDA cross-file definitions for LSP resolution in pass3 + cLSPIdx *cLSPDefIndex + // compileFlags holds per-file compile flags from compile_commands.json (C/C++ only) + compileFlags CompileFlagsMap } // New creates a new Pipeline. @@ -178,6 +182,9 @@ func (p *Pipeline) Run() error { return err } + // Load compile_commands.json for C/C++ include paths and defines + p.compileFlags = loadCompileCommands(p.RepoPath) + // Discover source files (filesystem, no DB — runs outside transaction) discoverOpts := &discover.Options{Mode: p.Mode} if p.Mode == discover.ModeFast { @@ -309,6 +316,7 @@ func (p *Pipeline) runFullPasses(files []discover.FileInfo) error { if p.goLSPIdx != nil { p.goLSPIdx.integrateThirdPartyDeps(p.RepoPath, p.importMaps) } + p.cLSPIdx = p.buildCLSPDefIndex() p.passCalls() slog.Info("pass.timing", "pass", "calls", "elapsed", time.Since(t)) // Release heavy fields no longer needed after call resolution. @@ -316,6 +324,7 @@ func (p *Pipeline) runFullPasses(files []discover.FileInfo) error { // (~160 KB/file → 16 GB for 100K-file repos). Nil them to halve peak RSS. p.releaseExtractionFields(fieldsPostCalls) p.goLSPIdx = nil // no longer needed after call resolution + p.cLSPIdx = nil logHeapStats("post_calls") if err := p.checkCancel(); err != nil { return err @@ -499,9 +508,11 @@ func (p *Pipeline) runIncrementalPasses( if p.goLSPIdx != nil { p.goLSPIdx.integrateThirdPartyDeps(p.RepoPath, p.importMaps) } + p.cLSPIdx = p.buildCLSPDefIndex() p.passCallsForFiles(filesToResolve) p.releaseExtractionFields(fieldsPostCalls) p.goLSPIdx = nil + p.cLSPIdx = nil p.passUsagesForFiles(filesToResolve) p.releaseExtractionFields(fieldsPostUsages) if err := p.checkCancel(); err != nil { @@ -1090,7 +1101,7 @@ func (p *Pipeline) passDefinitions(files []discover.FileInfo) { if p.ctx.Err() != nil { return } - results[i] = cbmParseFile(p.ProjectName, f) + results[i] = cbmParseFile(p.ProjectName, f, p.getCompileFlags(f.RelPath)) pf.advance(i + 1) }() } @@ -1107,6 +1118,8 @@ func (p *Pipeline) passDefinitions(files []discover.FileInfo) { "extract_total", time.Duration(profile.ExtractNs), "parse_avg_us", profile.ParseNs/profile.Files/1000, "extract_avg_us", profile.ExtractNs/profile.Files/1000, + "preprocess_total", time.Duration(profile.PreprocessNs), + "files_preprocessed", profile.FilesPreprocessed, ) } diff --git a/internal/pipeline/pipeline_cbm.go b/internal/pipeline/pipeline_cbm.go index 42d07bd7c..ffd232c6b 100644 --- a/internal/pipeline/pipeline_cbm.go +++ b/internal/pipeline/pipeline_cbm.go @@ -22,17 +22,17 @@ type cachedExtraction struct { // cbmParseFile reads a file, calls cbm.ExtractFile(), and converts the // result to the same parseResult format used by the batch write infrastructure. // This replaces parseFileAST() — all AST walking happens in C. -func cbmParseFile(projectName string, f discover.FileInfo) *parseResult { +func cbmParseFile(projectName string, f discover.FileInfo, flags *cbm.ExtractionFlags) *parseResult { source, cleanup, err := mmapFile(f.Path) if cleanup != nil { defer cleanup() } - return cbmParseFileFromSource(projectName, f, source, err) + return cbmParseFileFromSource(projectName, f, source, err, flags) } // cbmParseFileFromSource is like cbmParseFile but takes pre-read source data. // Used by the producer-consumer pipeline where I/O and CPU are separated. -func cbmParseFileFromSource(projectName string, f discover.FileInfo, source []byte, readErr error) *parseResult { +func cbmParseFileFromSource(projectName string, f discover.FileInfo, source []byte, readErr error, flags *cbm.ExtractionFlags) *parseResult { result := &parseResult{File: f} if readErr != nil { @@ -43,7 +43,7 @@ func cbmParseFileFromSource(projectName string, f discover.FileInfo, source []by // Strip UTF-8 BOM if present (common in C#/Windows-generated files) source = stripBOM(source) - cbmResult, err := cbm.ExtractFile(source, f.Language, projectName, f.RelPath) + cbmResult, err := cbm.ExtractFileWithFlags(source, f.Language, projectName, f.RelPath, flags) if err != nil { slog.Warn("cbm.extract.err", "path", f.RelPath, "lang", f.Language, "err", err) result.Err = err @@ -240,6 +240,9 @@ func (p *Pipeline) resolveFileCallsCBM(relPath string, ext *cachedExtraction) [] // Cross-file LSP resolution for Go files p.runGoLSPCrossFileResolution(ext, moduleQN, relPath, importMap) + // Cross-file LSP resolution for C/C++/CUDA files + p.runCLSPCrossFileResolution(ext, moduleQN, relPath) + // Build type map from CBM type assignments typeMap := inferTypesCBM(ext.Result.TypeAssigns, p.registry, moduleQN, importMap) @@ -276,6 +279,31 @@ func (p *Pipeline) runGoLSPCrossFileResolution(ext *cachedExtraction, moduleQN, } } +// runCLSPCrossFileResolution re-runs LSP with cross-file definitions from included headers. +func (p *Pipeline) runCLSPCrossFileResolution(ext *cachedExtraction, moduleQN, relPath string) { + if !isCOrCPP(ext.Language) || p.cLSPIdx == nil { + return + } + incDirs := p.getRelativeIncludeDirs(relPath) + if incDirs == nil { + incDirs = p.getAllRelativeIncludeDirs() + } + crossDefs := p.cLSPIdx.collectCrossFileDefs(ext.Result.Imports, relPath, incDirs) + if len(crossDefs) == 0 { + return + } + source := readFileSource(p.RepoPath, relPath) + if len(source) == 0 { + return + } + cppMode := ext.Language == lang.CPP || ext.Language == lang.CUDA + fileDefs := cbm.DefsToLSPDefs(ext.Result.Definitions, moduleQN) + resolved := cbm.RunCLSPCrossFile(source, moduleQN, cppMode, fileDefs, crossDefs, ext.Result.Imports) + if len(resolved) > 0 { + ext.Result.ResolvedCalls = resolved + } +} + // collectLSPResolvedEdges converts LSP-resolved calls to edges and builds a caller+method dedup set. func collectLSPResolvedEdges(resolvedCalls []cbm.ResolvedCall) (edges []resolvedEdge, lspCallerMethods map[string]bool) { lspResolved := make(map[string]bool)