fix(lsp): bound repeated resolver work without truncating large files

Memoize C expression evaluation, index repeated scope and registry queries,
and report incomplete walks explicitly on memo or depth failure.

Refs #1527

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-12 21:29:00 +02:00
1 parent 5d4b50fe1c
commit c3c97a99d7
21 files changed
+2493 -282

No files matched your search

+185 -18
View File
@@ -21,6 +21,7 @@
#include "lsp/java_lsp.h"
#include "lsp/kotlin_lsp.h"
#include "lsp/rust_lsp.h"
#include "lsp/lsp_work.h"
#include "preprocessor.h"
#include "sql_values.h" // #1735: literal INSERT rows kept out of the SQL parse
#include "cpp_branch_views.h"
@@ -2815,6 +2816,183 @@ static void extract_cpp_branch_views(const CBMExtractCtx *raw, const TSLanguage
}
}
/* ── Innermost enclosing Function/Method per call (#1527) ──────────────
* The call-context metrics attribute each call to the Function/Method def with
* the SMALLEST line span containing the call's line, ties to the lowest def
* index. Scanning every def for every call made that O(calls x defs) per file
* -- quadratic in file size, the largest single cost on a module with
* thousands of methods. The sweep below gives the identical answer in
* O((calls + defs) log defs): calls in line order, candidates entering a
* min-heap keyed (span, index) once their start line is reached and leaving it
* lazily once their end line has passed -- lines only grow, so a def that has
* ended never contains a later call. The heap top is then the smallest
* (span, index) among the defs that contain the line. */
static bool enclosing_is_callable(const CBMDefinition *d) {
return d->name && d->label &&
(strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0);
}
/* The reference answer for one line (and the path when the sweep's arrays
* cannot be allocated). */
static int cbm_enclosing_callable_scan(const CBMFileResult *result, int line) {
int best = -1;
int best_span = -1;
for (int di = 0; di < result->defs.count; di++) {
const CBMDefinition *d = &result->defs.items[di];
CBM_LSP_WORK(1);
if (!enclosing_is_callable(d)) {
continue;
}
if ((int)d->start_line <= line && line <= (int)d->end_line) {
int span = (int)d->end_line - (int)d->start_line;
if (best < 0 || span < best_span) {
best_span = span;
best = di;
}
}
}
return best;
}
static CBM_TLS const CBMFileResult *tl_enclosing_result;
static int enclosing_def_by_start(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
uint32_t la = tl_enclosing_result->defs.items[ia].start_line;
uint32_t lb = tl_enclosing_result->defs.items[ib].start_line;
return la < lb ? -1 : la > lb ? 1 : ia - ib;
}
static int enclosing_call_by_line(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
int la = tl_enclosing_result->calls.items[ia].start_line;
int lb = tl_enclosing_result->calls.items[ib].start_line;
return la < lb ? -1 : la > lb ? 1 : ia - ib;
}
/* Heap order: smaller span first, then smaller def index. */
static bool enclosing_heap_less(const CBMFileResult *r, int a, int b) {
const CBMDefinition *da = &r->defs.items[a];
const CBMDefinition *db = &r->defs.items[b];
int sa = (int)da->end_line - (int)da->start_line;
int sb = (int)db->end_line - (int)db->start_line;
return sa != sb ? sa < sb : a < b;
}
static void enclosing_heap_push(const CBMFileResult *r, int *heap, int *n, int di) {
int i = (*n)++;
heap[i] = di;
while (i > 0) {
int parent = (i - 1) / 2;
CBM_LSP_WORK(1);
if (!enclosing_heap_less(r, heap[i], heap[parent])) {
break;
}
int t = heap[i];
heap[i] = heap[parent];
heap[parent] = t;
i = parent;
}
}
static void enclosing_heap_pop(const CBMFileResult *r, int *heap, int *n) {
heap[0] = heap[--(*n)];
int i = 0;
for (;;) {
int l = (2 * i) + 1;
int m = i;
CBM_LSP_WORK(1);
if (l < *n && enclosing_heap_less(r, heap[l], heap[m])) {
m = l;
}
if (l + 1 < *n && enclosing_heap_less(r, heap[l + 1], heap[m])) {
m = l + 1;
}
if (m == i) {
break;
}
int t = heap[i];
heap[i] = heap[m];
heap[m] = t;
i = m;
}
}
/* enclosing[ci] = innermost Function/Method def index for call ci, or -1.
* Returns NULL when the working arrays cannot be allocated; the caller then
* asks cbm_enclosing_callable_scan per call (same answer, old cost). */
static int *cbm_enclosing_callables(const CBMFileResult *result, int call_count) {
int def_count = result->defs.count;
if (call_count <= 0) {
return NULL;
}
size_t call_bytes = (size_t)call_count * sizeof(int);
size_t def_bytes = (size_t)(def_count > 0 ? def_count : 1) * sizeof(int);
int *enclosing = cbm_alloc(CBM_MEM_CLASS_OTHER, call_bytes);
int *call_order = cbm_alloc(CBM_MEM_CLASS_OTHER, call_bytes);
int *cands = cbm_alloc(CBM_MEM_CLASS_OTHER, def_bytes);
int *heap = cbm_alloc(CBM_MEM_CLASS_OTHER, def_bytes);
if (!enclosing || !call_order || !cands || !heap) {
cbm_free(CBM_MEM_CLASS_OTHER, enclosing);
cbm_free(CBM_MEM_CLASS_OTHER, call_order);
cbm_free(CBM_MEM_CLASS_OTHER, cands);
cbm_free(CBM_MEM_CLASS_OTHER, heap);
return NULL;
}
int ncand = 0;
for (int di = 0; di < def_count; di++) {
if (enclosing_is_callable(&result->defs.items[di])) {
cands[ncand++] = di;
}
}
for (int ci = 0; ci < call_count; ci++) {
call_order[ci] = ci;
enclosing[ci] = -1;
}
tl_enclosing_result = result;
qsort(cands, (size_t)ncand, sizeof(int), enclosing_def_by_start);
qsort(call_order, (size_t)call_count, sizeof(int), enclosing_call_by_line);
tl_enclosing_result = NULL;
int heap_n = 0;
int next = 0;
for (int k = 0; k < call_count; k++) {
int ci = call_order[k];
int line = result->calls.items[ci].start_line;
while (next < ncand && (int)result->defs.items[cands[next]].start_line <= line) {
enclosing_heap_push(result, heap, &heap_n, cands[next++]);
}
while (heap_n > 0 && (int)result->defs.items[heap[0]].end_line < line) {
enclosing_heap_pop(result, heap, &heap_n);
}
enclosing[ci] = heap_n > 0 ? heap[0] : -1;
}
cbm_free(CBM_MEM_CLASS_OTHER, call_order);
cbm_free(CBM_MEM_CLASS_OTHER, cands);
cbm_free(CBM_MEM_CLASS_OTHER, heap);
return enclosing;
}
#ifdef CBM_ENABLE_TEST_SEAMS
int cbm_test_enclosing_sweep_mismatches(const CBMFileResult *result) {
int *enclosing = cbm_enclosing_callables(result, result->calls.count);
if (!enclosing) {
return result->calls.count > 0 ? -1 : 0;
}
int mismatches = 0;
for (int ci = 0; ci < result->calls.count; ci++) {
int line = result->calls.items[ci].start_line;
if (line > 0 && enclosing[ci] != cbm_enclosing_callable_scan(result, line)) {
mismatches++;
}
}
cbm_free(CBM_MEM_CLASS_OTHER, enclosing);
return mismatches;
}
#endif
static CBMFileResult *extract_file_ex_body(const char *source, int source_len, CBMLanguage language,
const char *project, const char *rel_path,
int64_t timeout_micros, const char **extra_defines,
@@ -3263,8 +3441,10 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
// Also run LSP on expanded source for additional type-resolved
// calls (language is already C/C++/CUDA — checked in enclosing
// block). Runs in every mode.
cbm_run_c_lsp(a, result, expanded, expanded_len, pp_root,
language != CBM_LANG_C, CBM_SOURCE_ORIGIN_PREPROCESSED);
if (!result->lsp_skipped) {
cbm_run_c_lsp(a, result, expanded, expanded_len, pp_root,
language != CBM_LANG_C, CBM_SOURCE_ORIGIN_PREPROCESSED);
}
/* All C-LSP emitters stamp origin directly so rewrite-time
* comparisons are already safe. Keep this boundary sweep as
@@ -3587,28 +3767,14 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
d->param_count = pc;
}
int *enclosing = cbm_enclosing_callables(result, orig_calls_count);
for (int ci = 0; ci < orig_calls_count; ci++) {
const CBMCall *c = &result->calls.items[ci];
if (!c->callee_name || c->start_line <= 0) {
continue;
}
// Innermost enclosing Function/Method def by line range (smallest span).
int best = -1;
int best_span = -1;
for (int di = 0; di < def_count; di++) {
const CBMDefinition *d = &result->defs.items[di];
if (!d->name || !d->label ||
(strcmp(d->label, "Function") != 0 && strcmp(d->label, "Method") != 0)) {
continue;
}
if ((int)d->start_line <= c->start_line && c->start_line <= (int)d->end_line) {
int span = (int)d->end_line - (int)d->start_line;
if (best < 0 || span < best_span) {
best_span = span;
best = di;
}
}
}
int best = enclosing ? enclosing[ci] : cbm_enclosing_callable_scan(result, c->start_line);
if (best < 0) {
continue;
}
@@ -3653,6 +3819,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
}
free(has_self);
free(has_guarded);
cbm_free(CBM_MEM_CLASS_OTHER, enclosing);
uint64_t t2 = now_ns();
+15
View File
@@ -537,6 +537,14 @@ typedef struct {
int cap;
} CBMResolvedCallArray;
/* Refinement status: existing parser/no-op exits are separate from these
* explicit evaluator failures. The caller retains prior completed output. */
typedef enum {
CBM_LSP_COMPLETE = 0,
CBM_LSP_MEMO_FAILED,
CBM_LSP_DEPTH_EXCEEDED,
} CBMLSPStatus;
// Growable arrays used during extraction.
typedef struct {
CBMDefinition *items;
@@ -1192,6 +1200,13 @@ void cbm_stringref_push(CBMStringRefArray *arr, CBMArena *a, CBMStringRef sr);
void cbm_infrabinding_push(CBMInfraBindingArray *arr, CBMArena *a, CBMInfraBinding ib);
void cbm_k8sfact_push(CBMK8sFactArray *arr, CBMArena *a, CBMK8sFact f);
void cbm_impltrait_push(CBMImplTraitArray *arr, CBMArena *a, CBMImplTrait it);
#ifdef CBM_ENABLE_TEST_SEAMS
/* #1527 seam: how many of result's calls get a different innermost enclosing
* Function/Method from the call-context sweep than from the reference
* per-call scan (0 = identical), or -1 when the sweep could not allocate. */
int cbm_test_enclosing_sweep_mismatches(const CBMFileResult *result);
#endif
void cbm_resolvedcall_push(CBMResolvedCallArray *arr, CBMArena *a, CBMResolvedCall rc);
void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch);
void cbm_diagram_arrows_push(CBMDiagramArrowArray *arr, CBMArena *a, CBMDiagramArrow da);
+438 -69
View File
@@ -1,10 +1,12 @@
#include "c_lsp.h"
#include "lsp_node_iter.h"
#include "lsp_work.h"
#include "../helpers.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <limits.h>
/* Safe kind accessor — returns CBM_TYPE_UNKNOWN for NULL types.
* Prevents SEGV in c_eval_expr_type_inner on unusual C++ AST shapes. */
@@ -12,6 +14,47 @@ static inline CBMTypeKind safe_kind(const CBMType *t) {
return t ? t->kind : CBM_TYPE_UNKNOWN;
}
#ifdef CBM_ENABLE_TEST_SEAMS
static _Thread_local int c_test_memo_allocations_left = -1;
static _Thread_local bool c_test_depth_failure;
void cbm_c_lsp_test_depth_fail(bool enabled) {
c_test_depth_failure = enabled;
}
static _Thread_local uint32_t c_test_walks;
uint32_t cbm_c_lsp_test_walks_take(void) {
uint32_t walks = c_test_walks;
c_test_walks = 0;
return walks;
}
void cbm_c_lsp_test_memo_fail_after(int successful_allocations) {
c_test_memo_allocations_left = successful_allocations;
}
#endif
static void c_memo_test_stage(CLSPContext *ctx, const char *stage) {
#ifdef CBM_ENABLE_TEST_SEAMS
c_test_walks++;
const char *depth_stage = getenv("CBM_TEST_C_LSP_DEPTH_FAIL_STAGE");
if (depth_stage && strcmp(depth_stage, stage) == 0)
ctx->eval_depth = 257;
const char *fail_stage = getenv("CBM_TEST_C_LSP_MEMO_FAIL_STAGE");
if (fail_stage && strcmp(fail_stage, stage) == 0)
ctx->test_memo_allocations_left = 0;
#else
(void)ctx;
(void)stage;
#endif
}
void cbm_c_lsp_record_failure(CBMFileResult *result, CBMLSPStatus status) {
result->has_error = true;
result->lsp_skipped = true;
/* Static lifetime keeps the reason available even when the arena is full.
* Result compaction/spilling copies strings, including non-arena strings. */
result->error_msg =
status == CBM_LSP_DEPTH_EXCEEDED ? CBM_C_LSP_DEPTH_ERROR : CBM_C_LSP_MEMO_ERROR;
}
// 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,
@@ -61,6 +104,11 @@ void c_lsp_init(CLSPContext *ctx, CBMArena *arena, const char *source, int sourc
const CBMTypeRegistry *registry, const char *module_qn, bool cpp_mode,
CBMResolvedCallArray *out) {
memset(ctx, 0, sizeof(CLSPContext));
#ifdef CBM_ENABLE_TEST_SEAMS
ctx->test_memo_allocations_left = c_test_memo_allocations_left;
if (c_test_depth_failure)
ctx->eval_depth = 257;
#endif
ctx->arena = arena;
ctx->source = source;
ctx->source_len = source_len;
@@ -193,14 +241,7 @@ static bool c_scope_declares_name(const CBMScope *scope, const char *var_name) {
if (!scope || !var_name) {
return false;
}
for (const CBMScopeChunk *chunk = scope->chunks; chunk; chunk = chunk->next) {
for (int i = 0; i < chunk->used; i++) {
if (chunk->bindings[i].name && strcmp(chunk->bindings[i].name, var_name) == 0) {
return true;
}
}
}
return false;
return cbm_scope_lookup_local(scope, var_name) != NULL;
}
/* Return the exact fp-state entry for the nearest lexical declaration. An
@@ -1523,26 +1564,129 @@ static const char *type_to_qn(const CBMType *t) {
static const CBMType *c_eval_expr_type_inner(CLSPContext *ctx, TSNode node);
/* Stack guard: the evaluator recurses once per expression nesting level.
* There is deliberately NO per-file work budget (#1527): the 10000-step cap
* that used to sit here (#323) left every later expression in a large file
* unresolved while the file still reported status `indexed`. What it was
* containing -- a fluent chain re-evaluating its receiver at every link, 2^N
* evaluations (the Bitcoin versionbits hang) -- is removed at the root by the
* per-evaluation memo below. */
#define C_EVAL_DEPTH_LIMIT 256
#define C_EVAL_MAX_STEPS_PER_FILE 10000
enum { C_EVAL_MEMO_INITIAL_CAP = 64 };
static uint32_t c_eval_memo_hash(const void *id) {
uintptr_t v = (uintptr_t)id >> 3; /* node structs are >=8-byte aligned */
return (uint32_t)((v ^ (v >> 29)) * 2654435761u);
}
/* Empty the memo for a new outermost evaluation: O(slots the last one used). */
static void c_eval_memo_reset(CLSPContext *ctx) {
for (int i = 0; i < ctx->eval_memo_count; i++) {
ctx->eval_memo[ctx->eval_memo_used[i]].node_id = NULL;
}
ctx->eval_memo_count = 0;
}
static const CBMType *c_eval_memo_lookup(const CLSPContext *ctx, const void *id) {
if (ctx->eval_memo_cap == 0) {
return NULL;
}
uint32_t mask = (uint32_t)ctx->eval_memo_cap - 1;
for (uint32_t i = c_eval_memo_hash(id) & mask;; i = (i + 1) & mask) {
const struct CLSPEvalMemoSlot *e = &ctx->eval_memo[i];
if (!e->node_id) {
return NULL;
}
if (e->node_id == id) {
return e->result;
}
}
}
static void c_eval_memo_place(CLSPContext *ctx, const void *id, const CBMType *result) {
uint32_t mask = (uint32_t)ctx->eval_memo_cap - 1;
uint32_t i = c_eval_memo_hash(id) & mask;
while (ctx->eval_memo[i].node_id) {
i = (i + 1) & mask;
}
ctx->eval_memo[i].node_id = id;
ctx->eval_memo[i].result = result;
ctx->eval_memo_used[ctx->eval_memo_count++] = (int)i;
}
static void *c_eval_memo_alloc(CLSPContext *ctx, size_t size) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (ctx->test_memo_allocations_left == 0)
return NULL;
if (ctx->test_memo_allocations_left > 0)
ctx->test_memo_allocations_left--;
#endif
return cbm_arena_alloc(ctx->arena, size);
}
/* Memoization is required to bound receiver re-evaluation. An allocation
* failure stops this file's refinement and is reported to the caller. */
static bool c_eval_memo_insert(CLSPContext *ctx, const void *id, const CBMType *result) {
if (ctx->eval_memo_count >= ctx->eval_memo_cap / 2) {
if (ctx->eval_memo_cap > INT_MAX / 2)
return false;
int new_cap = ctx->eval_memo_cap ? ctx->eval_memo_cap * 2 : C_EVAL_MEMO_INITIAL_CAP;
if ((size_t)new_cap > SIZE_MAX / sizeof(struct CLSPEvalMemoSlot) ||
(size_t)(new_cap / 2) > SIZE_MAX / sizeof(int))
return false;
struct CLSPEvalMemoSlot *slots = c_eval_memo_alloc(ctx, (size_t)new_cap * sizeof(*slots));
if (!slots)
return false;
int *used = c_eval_memo_alloc(ctx, (size_t)(new_cap / 2) * sizeof(int));
if (!used)
return false;
memset(slots, 0, (size_t)new_cap * sizeof(*slots));
struct CLSPEvalMemoSlot *old = ctx->eval_memo;
const int *old_used = ctx->eval_memo_used;
int old_count = ctx->eval_memo_count;
ctx->eval_memo = slots;
ctx->eval_memo_used = used;
ctx->eval_memo_cap = new_cap;
ctx->eval_memo_count = 0;
for (int k = 0; k < old_count; k++) {
const struct CLSPEvalMemoSlot *e = &old[old_used[k]];
c_eval_memo_place(ctx, e->node_id, e->result);
}
}
c_eval_memo_place(ctx, id, result);
return true;
}
const CBMType *c_eval_expr_type(CLSPContext *ctx, TSNode node) {
if (ts_node_is_null(node))
if (ctx->eval_failure || ts_node_is_null(node))
return cbm_type_unknown();
/* Expression type evaluation is best-effort. Some recovery-mode C++ ASTs
* can repeatedly drive member/type lookup without increasing recursion
* depth. Keep a generous per-file work budget so pathological expressions
* degrade to unknown instead of hanging repository indexing. */
if (ctx->eval_depth > C_EVAL_DEPTH_LIMIT || ctx->eval_steps++ > C_EVAL_MAX_STEPS_PER_FILE) {
if (ctx->debug && ctx->eval_steps == C_EVAL_MAX_STEPS_PER_FILE + 2) {
fprintf(stderr, " [clsp] expression eval step budget exhausted; returning unknown\n");
}
if (ctx->eval_depth > C_EVAL_DEPTH_LIMIT) {
ctx->eval_truncations++;
ctx->eval_failure = CBM_LSP_DEPTH_EXCEEDED;
return cbm_type_unknown();
}
if (ctx->eval_depth == 0) {
c_eval_memo_reset(ctx); /* scope may have changed since the last evaluation */
}
const CBMType *cached = c_eval_memo_lookup(ctx, node.id);
if (cached) {
return cached;
}
CBM_LSP_WORK(1); /* one real evaluation (complexity tests) */
uint32_t trunc_before = ctx->eval_truncations;
ctx->eval_depth++;
const CBMType *result = c_eval_expr_type_inner(ctx, node);
ctx->eval_depth--;
return result ? result : cbm_type_unknown();
if (!result) {
result = cbm_type_unknown();
}
/* Never memoize a result a depth cutoff may have truncated: the incomplete walk
* must stop before a second receiver branch repeats the truncated work. */
if (!ctx->eval_failure && ctx->eval_truncations == trunc_before &&
!c_eval_memo_insert(ctx, node.id, result)) {
ctx->eval_failure = CBM_LSP_MEMO_FAILED;
}
return ctx->eval_failure ? cbm_type_unknown() : result;
}
static const CBMType *c_eval_expr_type_inner(CLSPContext *ctx, TSNode node) {
@@ -3170,6 +3314,8 @@ static const CBMType *c_parse_declaration_type(CLSPContext *ctx, TSNode decl_nod
}
void c_process_statement(CLSPContext *ctx, TSNode node) {
if (ctx->eval_failure)
return;
if (ts_node_is_null(node))
return;
const char *kind = ts_node_type(node);
@@ -4380,6 +4526,8 @@ static void c_resolve_calls_in_node_inner(CLSPContext *ctx, TSNode node);
* frames via c_adl_resolve). Past the cap the subtree is skipped — its calls
* stay unresolved, which is graceful degradation, not a crash. */
static void c_resolve_calls_in_node(CLSPContext *ctx, TSNode node) {
if (ctx->eval_failure)
return;
if (ctx->walk_depth >= C_LSP_MAX_WALK_DEPTH)
return;
ctx->walk_depth++;
@@ -5351,6 +5499,8 @@ static char *c_macro_wrapped_func_name(CLSPContext *ctx, char *func_name, TSNode
}
static void c_process_function(CLSPContext *ctx, TSNode func_node) {
if (ctx->eval_failure)
return;
if (c_process_configured_test_body(ctx, func_node))
return;
TSNode decl = ts_node_child_by_field_name(func_node, "declarator", 10);
@@ -5599,6 +5749,8 @@ static bool c_is_body_container(const char *kind) {
// 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 (ctx->eval_failure)
return;
if (ts_node_is_null(child))
return;
const char *ck = ts_node_type(child);
@@ -6117,6 +6269,8 @@ void c_lsp_process_file(CLSPContext *ctx, TSNode root) {
if (ts_node_is_null(root))
return;
int resolved_before = ctx->resolved_calls ? ctx->resolved_calls->count : 0;
// Collect top-level children once (O(n)); both passes reuse the array.
// Indexing ts_node_child(root,i) per iteration is O(i) → O(n²) on a wide root.
uint32_t kn = 0;
@@ -6124,13 +6278,13 @@ void c_lsp_process_file(CLSPContext *ctx, TSNode root) {
TSNode child; // Hoisted: prevents ASan stack-use-after-scope between passes
// Pass 1: process using declarations and global variables
for (uint32_t i = 0; i < kn; i++) {
for (uint32_t i = 0; i < kn && !ctx->eval_failure; i++) {
child = kids[i];
c_bind_file_scope_item(ctx, child, 0);
}
// Pass 2: process functions, namespaces, classes, templates
for (uint32_t i = 0; i < kn; i++) {
for (uint32_t i = 0; i < kn && !ctx->eval_failure; i++) {
child = kids[i];
c_process_body_child(ctx, child);
}
@@ -6138,6 +6292,8 @@ void c_lsp_process_file(CLSPContext *ctx, TSNode root) {
// Release the per-file negative-lookup memo (malloc-owned; only allocated in
// shared-registry mode). No-op when it was never populated.
c_neg_memo_free(ctx);
if (ctx->eval_failure && ctx->resolved_calls)
ctx->resolved_calls->count = resolved_before;
}
// ============================================================================
@@ -6414,6 +6570,112 @@ static bool c_test_owners_complete(const CLSPContext *ctx) {
return expected == ctx->test_owner_seen_count;
}
/* QN -> index of its FIRST registration in reg->types, for the single-file
* registration loop below (#1527). That loop asked "is this type registered
* yet, and where?" by scanning every registered type per Field and per
* Method -- O(defs x types) before the registry was indexed. Tracking the
* first index as types are added answers the same question in O(1); the
* types array may be reallocated as it grows, so indices, not pointers. */
typedef struct {
const char **qn; /* NULL = empty slot */
int *first;
int cap; /* power of two, or 0 when unavailable */
int count;
} CTypeFirstIndex;
static uint32_t c_type_first_hash(const char *s) {
uint32_t h = 2166136261u;
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
h ^= *p;
h *= 16777619u;
}
return h;
}
/* Slot holding qn, or the empty slot where it belongs. */
static int c_type_first_slot(const CTypeFirstIndex *ix, const char *qn) {
uint32_t mask = (uint32_t)ix->cap - 1;
uint32_t i = c_type_first_hash(qn) & mask;
while (ix->qn[i] && strcmp(ix->qn[i], qn) != 0) {
CBM_LSP_WORK(1);
i = (i + 1) & mask;
}
return (int)i;
}
static bool c_type_first_alloc(CTypeFirstIndex *ix, CBMArena *arena, int cap) {
const char **qn = (const char **)cbm_arena_alloc(arena, (size_t)cap * sizeof(*qn));
int *first = (int *)cbm_arena_alloc(arena, (size_t)cap * sizeof(*first));
if (!qn || !first) {
return false;
}
memset(qn, 0, (size_t)cap * sizeof(*qn));
ix->qn = qn;
ix->first = first;
ix->cap = cap;
ix->count = 0;
return true;
}
/* Record reg->types[idx]; an existing QN keeps its first index. On OOM the
* index turns itself off (cap 0) and callers fall back to the scan. */
static void c_type_first_note(CTypeFirstIndex *ix, CBMArena *arena, const CBMTypeRegistry *reg,
int idx) {
const char *qn = reg->types[idx].qualified_name;
if (ix->cap == 0 || !qn) {
return;
}
if ((ix->count + 1) * 2 > ix->cap) {
CTypeFirstIndex grown = {0};
if (!c_type_first_alloc(&grown, arena, ix->cap * 2)) {
ix->cap = 0;
return;
}
for (int i = 0; i < ix->cap; i++) {
if (ix->qn[i]) {
int s = c_type_first_slot(&grown, ix->qn[i]);
grown.qn[s] = ix->qn[i];
grown.first[s] = ix->first[i];
grown.count++;
}
}
*ix = grown;
}
int s = c_type_first_slot(ix, qn);
if (!ix->qn[s]) {
ix->qn[s] = qn;
ix->first[s] = idx;
ix->count++;
}
}
/* Index of the first registered type named qn, or -1. Same answer as the
* linear scan it replaces (which also serves as the OOM fallback). */
static int c_type_first_find(const CTypeFirstIndex *ix, const CBMTypeRegistry *reg,
const char *qn) {
if (ix->cap > 0) {
int s = c_type_first_slot(ix, qn);
return ix->qn[s] ? ix->first[s] : -1;
}
for (int ri = 0; ri < reg->type_count; ri++) {
CBM_LSP_WORK(1);
if (strcmp(reg->types[ri].qualified_name, qn) == 0) {
return ri;
}
}
return -1;
}
/* cbm_registry_add_type plus the first-index bookkeeping. */
static void c_add_type_indexed(CBMTypeRegistry *reg, CTypeFirstIndex *ix, CBMArena *arena,
CBMRegisteredType rt) {
int before = reg->type_count;
cbm_registry_add_type(reg, rt);
if (reg->type_count > before) {
c_type_first_note(ix, arena, reg, before);
}
}
void cbm_run_c_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root, bool cpp_mode, CBMSourceOrigin source_origin) {
(void)cbm_run_c_lsp_with_test_owners(arena, result, source, source_len, root,
@@ -6429,6 +6691,13 @@ bool cbm_run_c_lsp_with_test_owners(
!cbm_test_definition_owners_match(test_owners, source, source_len, cpp_mode,
result->module_qn)) return false;
/* An earlier walk of this file stopped on a memo or depth failure
* (#1527) and is not repeated. The failure is already reported through
* lsp_skipped, which the pipeline treats as unverifiable owners for a
* file with configured owners. */
if (result->lsp_skipped)
return true;
/* TSNode carries no authenticated source-byte provenance. For configured
* owners, parse the verified buffer here rather than trusting a supplied
* root whose numeric spans could coincide with a different expression. */
@@ -6457,6 +6726,18 @@ bool cbm_run_c_lsp_with_test_owners(
const char *module_qn = result->module_qn;
/* Scratch for the registration index and the registry's hash index; both
* die with this call (quadratic-pattern #6: never per-file index memory in
* a longer-lived arena). */
CBMArena idx_arena;
cbm_arena_init(&idx_arena);
CTypeFirstIndex first_ix = {0};
if (c_type_first_alloc(&first_ix, &idx_arena, 256)) {
for (int ti = 0; ti < reg.type_count; ti++) {
c_type_first_note(&first_ix, &idx_arena, &reg, ti);
}
}
// Register file's own definitions
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition *d = &result->defs.items[i];
@@ -6490,19 +6771,14 @@ bool cbm_run_c_lsp_with_test_owners(
rt.embedded_types = embedded;
}
}
cbm_registry_add_type(&reg, rt);
c_add_type_indexed(&reg, &first_ix, &idx_arena, rt);
}
// Field definitions → populate field_names/field_types on parent type
if (d->label && strcmp(d->label, "Field") == 0 && d->parent_class && d->return_type) {
// Find or add the parent type in registry
CBMRegisteredType *parent_rt = NULL;
for (int ri = 0; ri < reg.type_count; ri++) {
if (strcmp(reg.types[ri].qualified_name, d->parent_class) == 0) {
parent_rt = &reg.types[ri];
break;
}
}
int parent_idx = c_type_first_find(&first_ix, &reg, d->parent_class);
CBMRegisteredType *parent_rt = parent_idx >= 0 ? &reg.types[parent_idx] : NULL;
if (parent_rt) {
// Count existing fields
int existing = 0;
@@ -6569,14 +6845,14 @@ bool cbm_run_c_lsp_with_test_owners(
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(&reg, d->parent_class)) {
if (c_type_first_find(&first_ix, &reg, d->parent_class) < 0) {
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(&reg, auto_type);
c_add_type_indexed(&reg, &first_ix, &idx_arena, auto_type);
}
}
@@ -6584,16 +6860,38 @@ bool cbm_run_c_lsp_with_test_owners(
}
}
/* Hash-index the registry before the walk (#1527): unindexed, every
* resolver lookup scanned the stdlib plus all of this file's definitions,
* O(calls x defs) per file. First-registered chains keep each duplicated
* name resolving to the entry the scan returned; resolver additions after
* this point stay visible through the registry's tail scan. */
reg.index_first_registered = true;
cbm_registry_finalize_into(&reg, &idx_arena);
// Initialize context and run
CLSPContext ctx;
c_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, cpp_mode, &result->resolved_calls);
ctx.source_origin = source_origin;
ctx.test_definition_owners = test_owners;
c_memo_test_stage(&ctx,
source_origin == CBM_SOURCE_ORIGIN_PREPROCESSED ? "preprocessed" : "raw");
c_lsp_process_file(&ctx, root);
c_rewrite_proved_destructor_candidates(result);
bool owners_complete = c_test_owners_complete(&ctx);
if (owner_tree) ts_tree_delete(owner_tree);
bool owners_complete;
if (ctx.eval_failure) {
/* The walk stopped early and dropped its output (#1527). The failure
* is reported through lsp_skipped, which the pipeline already treats
* as unverifiable owners for a file with configured owners; an
* owner error found before the stop is still returned. */
cbm_c_lsp_record_failure(result, ctx.eval_failure);
owners_complete = !ctx.test_owner_error;
} else {
c_rewrite_proved_destructor_candidates(result);
owners_complete = c_test_owners_complete(&ctx);
}
cbm_arena_destroy(&idx_arena);
if (owner_tree)
ts_tree_delete(owner_tree);
return owners_complete;
}
@@ -6767,22 +7065,18 @@ CBMTypeRegistry *cbm_c_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, in
return reg;
}
void cbm_run_c_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
const char *module_qn, bool cpp_mode, CBMTypeRegistry *reg,
const char **include_paths, const char **include_ns_qns,
int include_count, TSTree *cached_tree,
CBMResolvedCallArray *out) {
(void)cbm_run_c_lsp_cross_with_registry_with_test_owners(
arena, source, source_len, module_qn, cpp_mode, reg, include_paths,
include_ns_qns, include_count, cached_tree, out, CBM_SOURCE_ORIGIN_RAW, NULL);
}
bool cbm_run_c_lsp_cross_with_registry_with_test_owners(CBMArena *arena, const char *source, int source_len,
const char *module_qn, bool cpp_mode, CBMTypeRegistry *reg,
const char **include_paths, const char **include_ns_qns,
int include_count, TSTree *cached_tree,
CBMResolvedCallArray *out, CBMSourceOrigin source_origin,
const CBMFileResult *test_owners) {
/* Shared body of the Tier-2 cross entry points. Returns false when the
* configured owner proof cannot be applied (see c_lsp.h); *status reports a
* memo or depth failure of the walk (#1527), which dropped the walk's output. */
static bool c_lsp_cross_with_registry_run(CBMArena *arena, const char *source, int source_len,
const char *module_qn, bool cpp_mode,
CBMTypeRegistry *reg, const char **include_paths,
const char **include_ns_qns, int include_count,
TSTree *cached_tree, CBMResolvedCallArray *out,
CBMSourceOrigin source_origin,
const CBMFileResult *test_owners,
CBMLSPStatus *status) {
*status = CBM_LSP_COMPLETE;
if ((test_owners && test_owners->has_test_definition_owners &&
source_origin != CBM_SOURCE_ORIGIN_RAW) ||
!cbm_test_definition_owners_match(test_owners, source, source_len, cpp_mode,
@@ -6818,34 +7112,67 @@ bool cbm_run_c_lsp_cross_with_registry_with_test_owners(CBMArena *arena, const c
for (int i = 0; i < include_count; i++) {
c_lsp_add_include(&ctx, include_paths[i], include_ns_qns[i]);
}
c_memo_test_stage(&ctx, "cross");
c_lsp_process_file(&ctx, root);
if (owns_tree) {
ts_tree_delete(tree);
}
*status = ctx.eval_failure;
return c_test_owners_complete(&ctx);
}
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, TSTree *cached_tree,
CBMResolvedCallArray *out) {
(void)cbm_run_c_lsp_cross_with_test_owners(
arena, source, source_len, module_qn, cpp_mode, defs, def_count, include_paths,
include_ns_qns, include_count, cached_tree, out, CBM_SOURCE_ORIGIN_RAW, NULL);
bool cbm_run_c_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
const char *module_qn, bool cpp_mode, CBMTypeRegistry *reg,
const char **include_paths, const char **include_ns_qns,
int include_count, TSTree *cached_tree,
CBMResolvedCallArray *out) {
return cbm_run_c_lsp_cross_with_registry_status(
arena, source, source_len, module_qn, cpp_mode, reg, include_paths, include_ns_qns,
include_count, cached_tree, out) == CBM_LSP_COMPLETE;
}
bool cbm_run_c_lsp_cross_with_test_owners(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, TSTree *cached_tree,
CBMResolvedCallArray *out, CBMSourceOrigin source_origin,
const CBMFileResult *test_owners) {
CBMLSPStatus cbm_run_c_lsp_cross_with_registry_status(
CBMArena *arena, const char *source, int source_len, const char *module_qn, bool cpp_mode,
CBMTypeRegistry *reg, const char **include_paths, const char **include_ns_qns,
int include_count, TSTree *cached_tree, CBMResolvedCallArray *out) {
CBMLSPStatus status;
(void)c_lsp_cross_with_registry_run(arena, source, source_len, module_qn, cpp_mode, reg,
include_paths, include_ns_qns, include_count, cached_tree,
out, CBM_SOURCE_ORIGIN_RAW, NULL, &status);
return status;
}
/* A walk stopped by a memo or depth failure (#1527) cannot have proved every
* configured owner, so it returns false: the caller discards the output and
* reports the owners as unverified. */
bool cbm_run_c_lsp_cross_with_registry_with_test_owners(
CBMArena *arena, const char *source, int source_len, const char *module_qn, bool cpp_mode,
CBMTypeRegistry *reg, const char **include_paths, const char **include_ns_qns,
int include_count, TSTree *cached_tree, CBMResolvedCallArray *out,
CBMSourceOrigin source_origin, const CBMFileResult *test_owners) {
CBMLSPStatus status;
bool owners_ok = c_lsp_cross_with_registry_run(arena, source, source_len, module_qn, cpp_mode,
reg, include_paths, include_ns_qns,
include_count, cached_tree, out, source_origin,
test_owners, &status);
return owners_ok && status == CBM_LSP_COMPLETE;
}
/* Shared body of the per-file-registry cross entry points; same contract as
* c_lsp_cross_with_registry_run. */
static bool c_lsp_cross_run(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, TSTree *cached_tree, CBMResolvedCallArray *out,
CBMSourceOrigin source_origin, const CBMFileResult *test_owners,
CBMLSPStatus *status) {
*status = CBM_LSP_COMPLETE;
if ((test_owners && test_owners->has_test_definition_owners &&
source_origin != CBM_SOURCE_ORIGIN_RAW) ||
!cbm_test_definition_owners_match(test_owners, source, source_len, cpp_mode,
module_qn)) return false;
if (!source || source_len == 0 || !out)
return false;
@@ -6897,21 +7224,60 @@ bool cbm_run_c_lsp_cross_with_test_owners(CBMArena *arena, const char *source, i
c_lsp_add_include(&ctx, include_paths[i], include_ns_qns[i]);
}
c_memo_test_stage(&ctx, "cross");
c_lsp_process_file(&ctx, root);
if (owns_tree) {
ts_tree_delete(tree);
}
*status = ctx.eval_failure;
return c_test_owners_complete(&ctx);
}
bool 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, TSTree *cached_tree,
CBMResolvedCallArray *out) {
return cbm_run_c_lsp_cross_status(arena, source, source_len, module_qn, cpp_mode, defs,
def_count, include_paths, include_ns_qns, include_count,
cached_tree, out) == CBM_LSP_COMPLETE;
}
CBMLSPStatus cbm_run_c_lsp_cross_status(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,
TSTree *cached_tree, CBMResolvedCallArray *out) {
CBMLSPStatus status;
(void)c_lsp_cross_run(arena, source, source_len, module_qn, cpp_mode, defs, def_count,
include_paths, include_ns_qns, include_count, cached_tree, out,
CBM_SOURCE_ORIGIN_RAW, NULL, &status);
return status;
}
/* Same owner contract as cbm_run_c_lsp_cross_with_registry_with_test_owners. */
bool cbm_run_c_lsp_cross_with_test_owners(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,
TSTree *cached_tree, CBMResolvedCallArray *out,
CBMSourceOrigin source_origin,
const CBMFileResult *test_owners) {
CBMLSPStatus status;
bool owners_ok = c_lsp_cross_run(arena, source, source_len, module_qn, cpp_mode, defs,
def_count, include_paths, include_ns_qns, include_count,
cached_tree, out, source_origin, test_owners, &status);
return owners_ok && status == CBM_LSP_COMPLETE;
}
// --- Batch cross-file LSP ---
void cbm_batch_c_lsp_cross(CBMArena *arena, CBMBatchCLSPFile *files, int file_count,
bool cbm_batch_c_lsp_cross(CBMArena *arena, CBMBatchCLSPFile *files, int file_count,
CBMResolvedCallArray *out) {
if (!files || file_count <= 0 || !out)
return;
return true;
bool complete = true;
for (int f = 0; f < file_count; f++) {
CBMBatchCLSPFile *file = &files[f];
memset(&out[f], 0, sizeof(CBMResolvedCallArray));
@@ -6927,10 +7293,12 @@ void cbm_batch_c_lsp_cross(CBMArena *arena, CBMBatchCLSPFile *files, int file_co
memset(&file_out, 0, sizeof(file_out));
// Delegate to existing per-file function
cbm_run_c_lsp_cross(&file_arena, file->source, file->source_len, file->module_qn,
file->cpp_mode, file->defs, file->def_count, file->include_paths,
file->include_ns_qns, file->include_count, file->cached_tree,
&file_out);
bool file_complete = cbm_run_c_lsp_cross(
&file_arena, file->source, file->source_len, file->module_qn, file->cpp_mode,
file->defs, file->def_count, file->include_paths, file->include_ns_qns,
file->include_count, file->cached_tree, &file_out);
complete = complete && file_complete;
// Copy results to output arena (must outlive per-file arena)
if (file_out.count > 0) {
@@ -6955,4 +7323,5 @@ void cbm_batch_c_lsp_cross(CBMArena *arena, CBMBatchCLSPFile *files, int file_co
cbm_arena_destroy(&file_arena);
}
return complete;
}
+53 -5
View File
@@ -118,12 +118,33 @@ typedef struct {
// READ-ONLY across resolve workers — never mutate it
// (and never store per-worker arena pointers into it)
bool debug;
int eval_depth; // recursion depth for c_eval_expr_type (crash guard)
int eval_steps; // total expression eval calls for current file (hang guard)
int eval_depth; // recursion depth for c_eval_expr_type (crash guard)
uint32_t eval_truncations; // depth-cap cutoffs; a truncated result is never memoized
// Per-evaluation memo (#1527): results of the sub-expressions evaluated
// during ONE outermost c_eval_expr_type call, keyed by node identity. The
// evaluator re-evaluates a call's receiver (member lookup, then template
// substitution), which made an N-link fluent chain cost 2^N; the per-file
// step cap that used to hide that is gone. Evaluation never mutates scope
// or context, so within one outermost call a node's type cannot change;
// the memo is emptied when the next outermost call starts, because the
// resolver walk mutates scope between calls.
struct CLSPEvalMemoSlot *eval_memo; // open addressing, arena-owned
int *eval_memo_used; // slots filled by the current evaluation
int eval_memo_cap; // power of two, or 0
int eval_memo_count;
CBMLSPStatus eval_failure; // sticky: stop refinement rather than repeat uncached work
#ifdef CBM_ENABLE_TEST_SEAMS
int test_memo_allocations_left; // -1 = normal; 0 = fail the next memo allocation
#endif
int walk_depth; // c_resolve_calls_in_node self-recursion (AST nesting)
int control_flow_depth; // if/loop/switch/catch nesting; assignments merge fail-closed
} CLSPContext;
struct CLSPEvalMemoSlot {
const void *node_id; // TSNode.id; NULL = empty
const CBMType *result;
};
// --- API ---
void c_lsp_init(CLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
@@ -151,8 +172,28 @@ const CBMType *c_simplify_type(CLSPContext *ctx, const CBMType *t, bool unwrap_p
void cbm_run_c_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root, bool cpp_mode, CBMSourceOrigin source_origin);
// A memo allocation/capacity failure is an explicit incomplete-refinement error.
#define CBM_C_LSP_DEPTH_ERROR "C/C++ expression recursion depth limit exceeded"
#define CBM_C_LSP_MEMO_ERROR "C/C++ expression memo allocation or capacity failure"
void cbm_c_lsp_record_failure(CBMFileResult *result, CBMLSPStatus status);
#ifdef CBM_ENABLE_TEST_SEAMS
// Thread-local, scoped to memo allocations only; restore with -1 after a test.
void cbm_c_lsp_test_memo_fail_after(int successful_allocations);
void cbm_c_lsp_test_depth_fail(bool enabled);
uint32_t cbm_c_lsp_test_walks_take(void);
#endif
// 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,
// Status variants distinguish memo/depth failure. Bool wrappers report success.
// Completed output before a failed walk is kept.
CBMLSPStatus cbm_run_c_lsp_cross_status(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,
TSTree *cached_tree, // NULL = parse internally
CBMResolvedCallArray *out);
bool 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,
TSTree *cached_tree, // NULL = parse internally
@@ -165,7 +206,14 @@ CBMTypeRegistry *cbm_c_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, in
// Cross-file LSP using a pre-built shared registry (Tier 2). Skips the
// per-file registry build; just parse + resolve.
void cbm_run_c_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
CBMLSPStatus cbm_run_c_lsp_cross_with_registry_status(
CBMArena *arena, const char *source, int source_len, const char *module_qn, bool cpp_mode,
CBMTypeRegistry *reg, // pre-built, finalized, READ-ONLY
const char **include_paths, const char **include_ns_qns, int include_count,
TSTree *cached_tree, // NULL = parse internally
CBMResolvedCallArray *out);
bool cbm_run_c_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
const char *module_qn, bool cpp_mode,
CBMTypeRegistry *reg, // pre-built, finalized, READ-ONLY
const char **include_paths, const char **include_ns_qns,
@@ -222,7 +270,7 @@ typedef struct {
// Process multiple C/C++ files' cross-file LSP in one CGo call.
// out must point to file_count pre-zeroed CBMResolvedCallArray structs.
void cbm_batch_c_lsp_cross(CBMArena *arena, CBMBatchCLSPFile *files, int file_count,
bool cbm_batch_c_lsp_cross(CBMArena *arena, CBMBatchCLSPFile *files, int file_count,
CBMResolvedCallArray *out);
#endif // CBM_LSP_C_LSP_H
+2 -7
View File
@@ -489,13 +489,8 @@ static const CBMScope *kt_scope_declaring_name(const CBMScope *scope, const char
if (!name)
return NULL;
for (const CBMScope *current = scope; current; current = current->parent) {
for (const CBMScopeChunk *chunk = current->chunks; chunk; chunk = chunk->next) {
for (int i = 0; i < chunk->used; i++) {
if (chunk->bindings[i].name && strcmp(chunk->bindings[i].name, name) == 0) {
return current;
}
}
}
if (cbm_scope_lookup_local(current, name))
return current;
}
return NULL;
}
+29
View File
@@ -39,4 +39,33 @@ static inline TSNode* cbm_lsp_collect_children(CBMArena* arena, TSNode node, uin
return kids;
}
/* Named-children variant (#1527): the same single cursor pass, keeping only
* named nodes, so `for (i) ts_node_named_child(node, i)` over a wide node (a
* module holding thousands of classes, a body holding thousands of
* statements) becomes O(n). Returns NULL and sets *out_n=0 for a node without
* named children or on OOM; callers that must not lose children on OOM fall
* back to indexed access. */
static inline TSNode *cbm_lsp_collect_named_children(CBMArena *arena, TSNode node,
uint32_t *out_n) {
uint32_t nc = ts_node_named_child_count(node);
*out_n = 0;
if (nc == 0)
return NULL;
TSNode *kids = (TSNode *)cbm_arena_alloc(arena, (size_t)nc * sizeof(TSNode));
if (!kids)
return NULL;
uint32_t kn = 0;
TSTreeCursor cur = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cur)) {
do {
TSNode c = ts_tree_cursor_current_node(&cur);
if (ts_node_is_named(c))
kids[kn++] = c;
} while (kn < nc && ts_tree_cursor_goto_next_sibling(&cur));
}
ts_tree_cursor_delete(&cur);
*out_n = kn;
return kids;
}
#endif /* CBM_LSP_NODE_ITER_H */
+24
View File
@@ -0,0 +1,24 @@
/*
* lsp_work.h — deterministic work counter for LSP complexity tests (#1527).
*
* Seam builds only (CBM_ENABLE_TEST_SEAMS): every name comparison a resolver
* primitive performs while searching (scope frame probes, enclosing-definition
* candidates) adds to a thread-local counter, so a test can assert that the
* work grows linearly with input size without reading a clock (O9). Product
* builds compile CBM_LSP_WORK to nothing.
*/
#ifndef CBM_LSP_WORK_H
#define CBM_LSP_WORK_H
#include <stdint.h>
#ifdef CBM_ENABLE_TEST_SEAMS
extern _Thread_local uint64_t cbm_lsp_work_steps;
#define CBM_LSP_WORK(n) (cbm_lsp_work_steps += (uint64_t)(n))
/* Read and reset the calling thread's counter. */
uint64_t cbm_lsp_work_take(void);
#else
#define CBM_LSP_WORK(n) ((void)0)
#endif
#endif /* CBM_LSP_WORK_H */
+290 -87
View File
@@ -13,6 +13,7 @@
#include "py_lsp.h"
#include "../cbm.h"
#include "../helpers.h"
#include "lsp_node_iter.h"
#include "tree_sitter/api.h"
#include <ctype.h>
#include <stdint.h>
@@ -25,14 +26,80 @@
* only from py_lsp.c, never compiled standalone. */
#include "py_builtins.c"
/* Guards for py_eval_expr_type — mirrors c_eval_expr_type's guard design
* (C_EVAL_DEPTH_LIMIT / C_EVAL_MAX_STEPS_PER_FILE in c_lsp.c). */
/* Stack guard for py_eval_expr_type (#720; same limit as C_EVAL_DEPTH_LIMIT).
* There is deliberately NO per-file work budget (#1527): the step cap that
* used to sit here stopped resolution after 10000 evaluations and left the
* rest of a large file silently unresolved while the file still reported
* status `indexed`. Memoized evaluation (#710), hashed scope frames, and
* cursor-based child access reduce the repeated work. Memo failure is reported
* explicitly; the existing depth bound now reports an incomplete walk too. */
#define PY_LSP_MAX_EVAL_DEPTH 256
#define PY_EVAL_MAX_STEPS_PER_FILE 10000
#include "lsp_work.h"
#ifdef CBM_ENABLE_TEST_SEAMS
static _Thread_local int py_test_memo_allocations_left = -1;
static _Thread_local bool py_test_depth_failure;
void cbm_py_lsp_test_depth_fail(bool enabled) {
py_test_depth_failure = enabled;
}
void cbm_py_lsp_test_memo_fail_after(int successful_allocations) {
py_test_memo_allocations_left = successful_allocations;
}
#endif
static void py_memo_test_stage(PyLSPContext *ctx, const char *stage) {
#ifdef CBM_ENABLE_TEST_SEAMS
const char *depth_stage = getenv("CBM_TEST_PY_LSP_DEPTH_FAIL_STAGE");
if (depth_stage && strcmp(depth_stage, stage) == 0)
ctx->eval_depth = 256;
const char *fail_stage = getenv("CBM_TEST_PY_LSP_MEMO_FAIL_STAGE");
if (fail_stage && strcmp(fail_stage, stage) == 0)
ctx->test_memo_allocations_left = 0;
#else
(void)ctx;
(void)stage;
#endif
}
void cbm_py_lsp_record_failure(CBMFileResult *result, CBMLSPStatus status) {
result->has_error = true;
result->lsp_skipped = true;
/* Static lifetime also works when the result arena cannot allocate. */
result->error_msg =
status == CBM_LSP_DEPTH_EXCEEDED ? CBM_PY_LSP_DEPTH_ERROR : CBM_PY_LSP_MEMO_ERROR;
}
// Forward decls
static void py_resolve_calls_in_inner(PyLSPContext *ctx, TSNode node);
/* Named children of one node for an index loop (#1527). ts_node_named_child(n, i)
* restarts tree-sitter's child iterator at the first child, so indexing every
* child of a wide node is O(n^2) -- a module of thousands of classes, or a body
* of thousands of statements, spent most of its resolve time re-walking its
* own children. Wide nodes are collected once (lsp_node_iter.h); narrow nodes
* keep plain indexed access, which is cheaper than an allocation. */
enum { PY_KIDS_COLLECT_MIN = 64 };
typedef struct {
TSNode node;
TSNode *kids; /* NULL = index the node directly */
uint32_t n;
} PyKids;
static PyKids py_kids(CBMArena *arena, TSNode node) {
PyKids k = {node, NULL, ts_node_named_child_count(node)};
if (k.n >= PY_KIDS_COLLECT_MIN) {
uint32_t got = 0;
TSNode *kids = cbm_lsp_collect_named_children(arena, node, &got);
if (kids && got == k.n)
k.kids = kids;
}
return k;
}
static TSNode py_kid(const PyKids *k, uint32_t i) {
return k->kids ? k->kids[i] : ts_node_named_child(k->node, i);
}
/* Decorators are extracted as raw syntax (`@property`, `@pkg.cache(...)`),
* not resolved qualified names. Preserve the raw array for sound callable-
* value admission, while parsing only enough of the spelling to retain the
@@ -142,7 +209,7 @@ static void py_mark_ambiguous_callable_bindings(CBMTypeRegistry *registry) {
* The walk_depth-- runs after the inner returns, so early returns in the body
* never leak the counter. */
static void py_resolve_calls_in(PyLSPContext *ctx, TSNode node) {
if (ctx->walk_depth >= cbm_lsp_max_walk_depth())
if (!ctx || ctx->eval_failure || ctx->walk_depth >= cbm_lsp_max_walk_depth())
return;
ctx->walk_depth++;
py_resolve_calls_in_inner(ctx, node);
@@ -229,6 +296,11 @@ void py_lsp_init(PyLSPContext *ctx, CBMArena *arena, const char *source, int sou
if (!ctx)
return;
memset(ctx, 0, sizeof(PyLSPContext));
#ifdef CBM_ENABLE_TEST_SEAMS
ctx->test_memo_allocations_left = py_test_memo_allocations_left;
if (py_test_depth_failure)
ctx->eval_depth = 256;
#endif
ctx->arena = arena;
ctx->source = source;
ctx->source_len = source_len;
@@ -505,16 +577,19 @@ static PyDirectImportKind py_import_kind_from_statement(PyLSPContext *ctx, TSNod
return py_import_match_result(&match, qn_io);
}
static PyDirectImportKind py_import_kind_from_ast(PyLSPContext *ctx, TSNode root, const char *local,
const char **qn_io) {
/* stmts: the root's import statements, collected once per root by the caller
* (#1527) -- only they can match, and rescanning every top-level statement
* once per import made classification O(imports x module size). */
static PyDirectImportKind py_import_kind_from_ast(PyLSPContext *ctx, bool have_root,
const TSNode *stmts, uint32_t stmt_count,
const char *local, const char **qn_io) {
const char *qn = qn_io ? *qn_io : NULL;
if (!ctx || !local || !qn || !qn_io || ts_node_is_null(root))
if (!ctx || !local || !qn || !qn_io || !have_root)
return PY_DIRECT_IMPORT_UNKNOWN;
PyImportSyntaxMatch match = {0};
uint32_t root_count = ts_node_named_child_count(root);
for (uint32_t i = 0; i < root_count; i++) {
py_import_match_statement(ctx, ts_node_named_child(root, i), local, qn, &match);
for (uint32_t i = 0; i < stmt_count; i++) {
py_import_match_statement(ctx, stmts[i], local, qn, &match);
}
return py_import_match_result(&match, qn_io);
}
@@ -561,12 +636,32 @@ static void py_bind_dotted_prefixes(PyLSPContext *ctx, const char *qn) {
static void py_classify_imports_for_root(PyLSPContext *ctx, TSNode root) {
if (!ctx)
return;
/* A null root classifies every import UNKNOWN, as it always has; so does
* a root whose statement list cannot be allocated (no proof either way). */
bool have_root = !ts_node_is_null(root);
TSNode *stmts = NULL;
uint32_t stmt_count = 0;
if (have_root && ctx->import_count > 0) {
PyKids rk = py_kids(ctx->arena, root);
if (rk.n > 0) {
stmts = (TSNode *)cbm_arena_alloc(ctx->arena, (size_t)rk.n * sizeof(TSNode));
if (!stmts)
have_root = false;
}
for (uint32_t i = 0; stmts && i < rk.n; i++) {
TSNode c = py_kid(&rk, i);
const char *ck = ts_node_type(c);
if (strcmp(ck, "import_statement") == 0 || strcmp(ck, "import_from_statement") == 0)
stmts[stmt_count++] = c;
}
}
for (int i = 0; i < ctx->import_count; i++) {
const char *local = ctx->import_local_names[i];
const char *qn = ctx->import_module_qns[i];
if (!local || !qn)
continue;
PyDirectImportKind direct_kind = py_import_kind_from_ast(ctx, root, local, &qn);
PyDirectImportKind direct_kind =
py_import_kind_from_ast(ctx, have_root, stmts, stmt_count, local, &qn);
ctx->import_module_qns[i] = qn;
if (ctx->import_kinds)
ctx->import_kinds[i] = (unsigned char)direct_kind;
@@ -783,6 +878,8 @@ static const char *py_lookup_dict_dispatch(PyLSPContext *ctx, const char *var, c
return NULL;
}
enum { PY_RESOLVED_DEDUP_WINDOW = 256 };
static void py_emit_resolved_call_reason(PyLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence, const char *reason,
TSNode site) {
@@ -796,9 +893,8 @@ static void py_emit_resolved_call_reason(PyLSPContext *ctx, const char *callee_q
// entries catches the common case while keeping per-emission O(1).
// Without this cap the dedup is O(N) per emission -> O(N^2) per file
// and dominates above ~1k call sites.
enum { DEDUP_WINDOW = 256 };
int n = ctx->resolved_calls->count;
int start = n > DEDUP_WINDOW ? n - DEDUP_WINDOW : 0;
int start = n > PY_RESOLVED_DEDUP_WINDOW ? n - PY_RESOLVED_DEDUP_WINDOW : 0;
for (int i = start; i < n; i++) {
CBMResolvedCall *rc = &ctx->resolved_calls->items[i];
if (rc->kind == CBM_RESOLVED_INVOCATION && rc->site_start_byte == site_start &&
@@ -1422,7 +1518,7 @@ static const CBMType *py_iterable_element_type(PyLSPContext *ctx, const CBMType
}
/* The real recursive-descent evaluator. Never call directly — go through
* the memoizing, depth- and budget-guarded py_eval_expr_type wrapper below
* the memoizing, depth-guarded py_eval_expr_type wrapper below
* (every recursive call inside this body already does). */
static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node) {
if (!ctx || ts_node_is_null(node))
@@ -2086,6 +2182,14 @@ static bool py_type_cache_grow(PyLSPContext *ctx) {
if (ctx->type_cache_cap >= PY_TYPE_CACHE_MAX_CAP)
return false;
int new_cap = ctx->type_cache_cap ? ctx->type_cache_cap * 2 : PY_TYPE_CACHE_INITIAL_CAP;
if ((size_t)new_cap > SIZE_MAX / sizeof(CBMPyTypeCacheEntry))
return false;
#ifdef CBM_ENABLE_TEST_SEAMS
if (ctx->test_memo_allocations_left == 0)
return false;
if (ctx->test_memo_allocations_left > 0)
ctx->test_memo_allocations_left--;
#endif
CBMPyTypeCacheEntry *new_entries = (CBMPyTypeCacheEntry *)cbm_arena_alloc(
ctx->arena, (size_t)new_cap * sizeof(CBMPyTypeCacheEntry));
if (!new_entries)
@@ -2111,9 +2215,9 @@ static bool py_type_cache_grow(PyLSPContext *ctx) {
return true;
}
static void py_type_cache_insert(PyLSPContext *ctx, const void *id, const CBMType *result) {
static bool py_type_cache_insert(PyLSPContext *ctx, const void *id, const CBMType *result) {
if (!id || !result)
return;
return true;
/* Keep load strictly below 75% so probe chains stay short. If growth
* fails (OOM / max cap), REJECT the insert rather than filling up: a
* full table would turn every probe loop into a full-table scan, and
@@ -2121,7 +2225,7 @@ static void py_type_cache_insert(PyLSPContext *ctx, const void *id, const CBMTyp
if (!ctx->type_cache || (ctx->type_cache_count + 1) * 4 > ctx->type_cache_cap * 3) {
if (!py_type_cache_grow(ctx) &&
(!ctx->type_cache || (ctx->type_cache_count + 1) * 4 > ctx->type_cache_cap * 3))
return;
return false;
}
uint32_t mask = (uint32_t)ctx->type_cache_cap - 1;
uint32_t idx = py_type_cache_hash(id) & mask;
@@ -2132,31 +2236,33 @@ static void py_type_cache_insert(PyLSPContext *ctx, const void *id, const CBMTyp
e->gen = ctx->type_cache_gen;
e->result = result;
ctx->type_cache_count++;
return;
return true;
}
if (e->node_id == id) {
e->gen = ctx->type_cache_gen; /* refresh a stale-generation entry in place */
e->result = result;
return;
return true;
}
idx = (idx + 1) & mask;
}
/* Probe bound exhausted — only reachable with a corrupt count; drop the
* insert instead of spinning. */
return false;
}
/* Memoizing, depth- and budget-guarded wrapper — the function every call
* site in this file goes through. */
/* Memoizing, depth-guarded wrapper — the function every call site in this
* file goes through. */
static const CBMType *py_eval_expr_type(PyLSPContext *ctx, TSNode node) {
if (!ctx || ts_node_is_null(node))
if (!ctx || ctx->eval_failure || ts_node_is_null(node))
return cbm_type_unknown();
/* Depth cap (issue #720): the evaluator recurses once per expression
* nesting level, so a pathologically deep expression (tens of
* thousands of parens) overflowed the native stack. Same limit as
* C_EVAL_DEPTH_LIMIT. Past the cap: unknown, and NEVER cached. */
* thousands of parens) overflowed the native stack. At the existing bound,
* stop this walk explicitly; never memoize a truncated result as complete. */
if (ctx->eval_depth >= PY_LSP_MAX_EVAL_DEPTH) {
ctx->eval_truncations++;
ctx->eval_failure = CBM_LSP_DEPTH_EXCEEDED;
return cbm_type_unknown();
}
@@ -2164,17 +2270,9 @@ static const CBMType *py_eval_expr_type(PyLSPContext *ctx, TSNode node) {
if (cached)
return cached;
/* Per-file work budget (mirrors C_EVAL_MAX_STEPS_PER_FILE): expression
* type evaluation is best-effort, so pathological files degrade to
* unknown instead of stalling repository indexing. Only real
* evaluations consume budget — cache hits above are O(1). */
if (ctx->eval_steps++ > PY_EVAL_MAX_STEPS_PER_FILE) {
ctx->eval_truncations++;
if (ctx->debug && ctx->eval_steps == PY_EVAL_MAX_STEPS_PER_FILE + 2) {
fprintf(stderr, " [pylsp] expression eval step budget exhausted; returning unknown\n");
}
return cbm_type_unknown();
}
/* One real (non-memoized) evaluation: the work the complexity tests count
* to prove evaluation stays linear without a budget. */
CBM_LSP_WORK(1);
uint32_t trunc_before = ctx->eval_truncations;
ctx->eval_depth++;
@@ -2184,19 +2282,25 @@ static const CBMType *py_eval_expr_type(PyLSPContext *ctx, TSNode node) {
result = cbm_type_unknown();
/* Only cache full-fidelity results: if any descendant evaluation was
* cut off by the depth cap or the step budget, this result may be a
* truncated `unknown`. Caching it would poison later evaluations that
* reach the same node from a shallower frame (or with fresh budget) —
* exactly the reuse the guards are supposed to preserve. */
if (ctx->eval_truncations == trunc_before)
py_type_cache_insert(ctx, node.id, result);
return result;
* cut off by the depth cap, this result may be a truncated `unknown`.
* The sticky failure stops this walk instead of repeating uncached branches;
* a fresh walk starts with a fresh cache. */
if (!ctx->eval_failure && ctx->eval_truncations == trunc_before &&
!py_type_cache_insert(ctx, node.id, result))
ctx->eval_failure = CBM_LSP_MEMO_FAILED;
return ctx->eval_failure ? cbm_type_unknown() : result;
}
#ifdef CBM_ENABLE_TEST_SEAMS
const CBMType *cbm_py_lsp_test_eval(PyLSPContext *ctx, TSNode node) {
return py_eval_expr_type(ctx, node);
}
#endif
/* ── statement processing: bind from assignments, for-loops, with-as ──── */
static void py_process_statement(PyLSPContext *ctx, TSNode node) {
if (!ctx || ts_node_is_null(node))
if (!ctx || ctx->eval_failure || ts_node_is_null(node))
return;
const char *k = ts_node_type(node);
@@ -2502,6 +2606,8 @@ static void py_process_statement(PyLSPContext *ctx, TSNode node) {
/* ── Recursive walker: process statements + emit resolved_calls ── */
static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
if (!ctx || ctx->eval_failure)
return;
if (!ctx || ctx->callable_value_proof_disabled)
return;
TSNode fn = ts_node_child_by_field_name(call_node, "function", 8);
@@ -3443,9 +3549,9 @@ static void py_resolve_calls_in_inner(PyLSPContext *ctx, TSNode node) {
return;
}
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
py_resolve_calls_in(ctx, ts_node_named_child(node, i));
PyKids nk = py_kids(ctx->arena, node);
for (uint32_t i = 0; i < nk.n; i++) {
py_resolve_calls_in(ctx, py_kid(&nk, i));
}
}
@@ -4006,6 +4112,8 @@ static void py_bind_parameters(PyLSPContext *ctx, TSNode params) {
}
static void py_process_function(PyLSPContext *ctx, TSNode func_node, const char *container_qn) {
if (!ctx || ctx->eval_failure)
return;
TSNode name_node = ts_node_child_by_field_name(func_node, "name", 4);
if (ts_node_is_null(name_node))
return;
@@ -4034,9 +4142,9 @@ static void py_process_function(PyLSPContext *ctx, TSNode func_node, const char
if (!ts_node_is_null(body)) {
py_resolve_calls_in(ctx, body);
// Also descend into nested function/class definitions in the body.
uint32_t bnc = ts_node_named_child_count(body);
for (uint32_t i = 0; i < bnc; i++) {
TSNode c = ts_node_named_child(body, i);
PyKids bk = py_kids(ctx->arena, body);
for (uint32_t i = 0; i < bk.n; i++) {
TSNode c = py_kid(&bk, i);
const char *ck = ts_node_type(c);
if (strcmp(ck, "function_definition") == 0) {
py_process_function(ctx, c, ctx->enclosing_func_qn);
@@ -4064,6 +4172,8 @@ static bool py_is_init_method(PyLSPContext *ctx, TSNode func_node) {
}
static void py_process_class(PyLSPContext *ctx, TSNode class_node) {
if (!ctx || ctx->eval_failure)
return;
TSNode name_node = ts_node_child_by_field_name(class_node, "name", 4);
if (ts_node_is_null(name_node))
return;
@@ -4076,13 +4186,14 @@ static void py_process_class(PyLSPContext *ctx, TSNode class_node) {
TSNode body = ts_node_child_by_field_name(class_node, "body", 4);
if (!ts_node_is_null(body)) {
uint32_t bnc = ts_node_named_child_count(body);
PyKids bk = py_kids(ctx->arena, body);
uint32_t bnc = bk.n;
// First pass: process class-level annotated assignments (PEP 526
// class-body field annotations like `x: int`) and dunder __init__
// methods so fields are registered before sibling methods that
// reference them.
for (uint32_t i = 0; i < bnc; i++) {
TSNode c = ts_node_named_child(body, i);
TSNode c = py_kid(&bk, i);
const char *ck = ts_node_type(c);
if (strcmp(ck, "expression_statement") == 0 && ts_node_named_child_count(c) > 0) {
TSNode inner = ts_node_named_child(c, 0);
@@ -4105,7 +4216,7 @@ static void py_process_class(PyLSPContext *ctx, TSNode class_node) {
}
// Second pass: __init__ (so self.x assignments populate fields).
for (uint32_t i = 0; i < bnc; i++) {
TSNode c = ts_node_named_child(body, i);
TSNode c = py_kid(&bk, i);
const char *ck = ts_node_type(c);
TSNode fn_node = c;
bool is_decorated = strcmp(ck, "decorated_definition") == 0;
@@ -4123,7 +4234,7 @@ static void py_process_class(PyLSPContext *ctx, TSNode class_node) {
}
// Third pass: every other method (and nested classes).
for (uint32_t i = 0; i < bnc; i++) {
TSNode c = ts_node_named_child(body, i);
TSNode c = py_kid(&bk, i);
const char *ck = ts_node_type(c);
if (strcmp(ck, "function_definition") == 0) {
if (!py_is_init_method(ctx, c)) {
@@ -4148,10 +4259,17 @@ static void py_process_class(PyLSPContext *ctx, TSNode class_node) {
/* Module bindings are replayed in source order before deferred function bodies
* are analyzed. Class definitions always replace an earlier callable identity;
* the registry decides whether the resulting class type is known precisely. */
static bool py_root_defines_class_named(PyLSPContext *ctx, TSNode root, const char *name) {
uint32_t count = ts_node_named_child_count(root);
for (uint32_t i = 0; i < count; i++) {
TSNode node = ts_node_named_child(root, i);
/* The names of the classes this source defines at module level, as a name set
* (a detached, hash-indexed CBMScope frame). Built in one pass over the root:
* asking "does the root define a class named X?" by rescanning the root once
* per registry type made module assembly O(types x top-level statements).
* Returns NULL if a class name or set entry cannot be collected completely. */
static CBMScope *py_root_class_names(PyLSPContext *ctx, const PyKids *rk) {
CBMScope *names = cbm_scope_push(ctx->arena, NULL);
if (!names)
return NULL;
for (uint32_t i = 0; i < rk->n; i++) {
TSNode node = py_kid(rk, i);
const char *kind = ts_node_type(node);
if (strcmp(kind, "decorated_definition") == 0) {
node = ts_node_child_by_field_name(node, "definition", 10);
@@ -4159,20 +4277,33 @@ static bool py_root_defines_class_named(PyLSPContext *ctx, TSNode root, const ch
}
if (strcmp(kind, "class_definition") != 0)
continue;
TSNode name_node = ts_node_child_by_field_name(node, "name", 4);
if (py_import_node_text_equals(ctx, name_node, name))
return true;
char *name = py_node_text(ctx, ts_node_child_by_field_name(node, "name", 4));
#ifdef CBM_ENABLE_TEST_SEAMS
if (ctx->test_root_name_failure == 1)
name = NULL;
else if (ctx->test_root_name_failure == 2)
name = "";
#endif
if (!name || !name[0] || !cbm_scope_bind_checked(names, name, cbm_type_unknown()))
return NULL;
}
return false;
return names;
}
/* A project registry can contain another file's class in the same logical
* Python module. Keep those cross-file globals available, but do not prebind
* classes declared by this source: their binding epoch belongs in the ordered
* replay below. */
static void py_bind_external_module_classes(PyLSPContext *ctx, TSNode root) {
static void py_bind_external_module_classes(PyLSPContext *ctx, const PyKids *rk) {
if (!ctx || !ctx->registry || !ctx->module_qn)
return;
CBMScope *defined = py_root_class_names(ctx, rk);
if (!defined) {
/* Without the set no external class can be proven undeclared here;
* binding none is the fail-closed direction. */
py_disable_callable_value_proof(ctx);
return;
}
size_t prefix_len = strlen(ctx->module_qn);
CBMTypeShortIter all_types;
cbm_registry_all_types_chain(ctx->registry, &all_types);
@@ -4181,13 +4312,20 @@ static void py_bind_external_module_classes(PyLSPContext *ctx, TSNode root) {
const char *qn = type->qualified_name;
const char *name = type->short_name;
if (!qn || !name || strncmp(qn, ctx->module_qn, prefix_len) != 0 || qn[prefix_len] != '.' ||
py_root_defines_class_named(ctx, root, name)) {
cbm_scope_lookup_local(defined, name)) {
continue;
}
py_scope_bind(ctx, name, cbm_type_named(ctx->arena, qn));
}
}
#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_py_lsp_test_bind_external_classes(PyLSPContext *ctx, TSNode root) {
PyKids rk = py_kids(ctx->arena, root);
py_bind_external_module_classes(ctx, &rk);
}
#endif
static void py_bind_module_class(PyLSPContext *ctx, TSNode class_node) {
if (!ctx || ts_node_is_null(class_node) || !ctx->module_qn)
return;
@@ -4564,9 +4702,9 @@ static void py_invalidate_possible_bindings(PyLSPContext *ctx, TSNode node, int
}
return;
}
uint32_t count = ts_node_named_child_count(node);
for (uint32_t i = 0; i < count; i++)
py_invalidate_possible_bindings(ctx, ts_node_named_child(node, i), depth + 1);
PyKids nk = py_kids(ctx->arena, node);
for (uint32_t i = 0; i < nk.n; i++)
py_invalidate_possible_bindings(ctx, py_kid(&nk, i), depth + 1);
}
static bool py_replayable_import_kind(PyDirectImportKind kind) {
@@ -4654,10 +4792,22 @@ static bool py_expression_is_annotation_only_assignment(TSNode statement) {
}
void py_lsp_process_file(PyLSPContext *ctx, TSNode root) {
if (!ctx || ts_node_is_null(root))
if (!ctx || ctx->eval_failure || ts_node_is_null(root))
return;
int resolved_start = ctx->resolved_calls ? ctx->resolved_calls->count : 0;
int synthetic_start = ctx->syn_calls ? ctx->syn_calls->count : 0;
/* Only invocation dedup mutates prior rows, within this fixed tail.
* Snapshot once per walk: no full-output copy or fallible rollback allocation. */
int tail_count =
resolved_start < PY_RESOLVED_DEDUP_WINDOW ? resolved_start : PY_RESOLVED_DEDUP_WINDOW;
int tail_start = resolved_start - tail_count;
CBMResolvedCall saved_tail[PY_RESOLVED_DEDUP_WINDOW];
CBMResolvedCall *original_tail = tail_count ? ctx->resolved_calls->items + tail_start : NULL;
if (tail_count)
memcpy(saved_tail, original_tail, (size_t)tail_count * sizeof(*saved_tail));
PyKids rk = py_kids(ctx->arena, root);
py_classify_imports_for_root(ctx, root);
py_bind_external_module_classes(ctx, root);
py_bind_external_module_classes(ctx, &rk);
unsigned char *consumed_imports = NULL;
if (ctx->import_count > 0) {
consumed_imports = (unsigned char *)cbm_arena_alloc(ctx->arena, (size_t)ctx->import_count);
@@ -4665,13 +4815,13 @@ void py_lsp_process_file(PyLSPContext *ctx, TSNode root) {
memset(consumed_imports, 0, (size_t)ctx->import_count);
}
uint32_t nc = ts_node_named_child_count(root);
uint32_t nc = rk.n;
const char *prev_func = ctx->enclosing_func_qn;
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.__module__", ctx->module_qn);
// Pass 1: execute top-level binding effects in source order. Function
// bodies remain deferred until the final module scope has been assembled.
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(root, i);
for (uint32_t i = 0; i < nc && !ctx->eval_failure; i++) {
TSNode c = py_kid(&rk, i);
const char *ck = ts_node_type(c);
if (strcmp(ck, "import_statement") == 0 || strcmp(ck, "import_from_statement") == 0) {
py_replay_import_statement(ctx, c, consumed_imports);
@@ -4709,8 +4859,8 @@ void py_lsp_process_file(PyLSPContext *ctx, TSNode root) {
}
}
// Pass 2: top-level calls (rare) and nested definitions.
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(root, i);
for (uint32_t i = 0; i < nc && !ctx->eval_failure; i++) {
TSNode c = py_kid(&rk, i);
const char *ck = ts_node_type(c);
if (strcmp(ck, "function_definition") == 0) {
py_process_function(ctx, c, NULL);
@@ -4729,6 +4879,17 @@ void py_lsp_process_file(PyLSPContext *ctx, TSNode root) {
}
}
ctx->enclosing_func_qn = prev_func;
if (ctx->eval_failure) {
if (tail_count) {
memcpy(original_tail, saved_tail, (size_t)tail_count * sizeof(*saved_tail));
memcpy(ctx->resolved_calls->items + tail_start, saved_tail,
(size_t)tail_count * sizeof(*saved_tail));
}
if (ctx->resolved_calls)
ctx->resolved_calls->count = resolved_start;
if (ctx->syn_calls)
ctx->syn_calls->count = synthetic_start;
}
}
/* Register one definition into the registry. Returns true if recognized. */
@@ -4887,7 +5048,7 @@ static bool py_register_def(CBMArena *arena, CBMTypeRegistry *reg, CBMDefinition
void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root) {
if (!arena || !result)
if (!arena || !result || result->lsp_skipped)
return;
/* Inject minimal builtin definitions as real graph nodes (builtins.len,
@@ -4913,6 +5074,18 @@ void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
}
py_mark_ambiguous_callable_bindings(&reg);
/* Hash-index the registry before the walk (#1527). Unfinalized, every
* lookup scanned the stdlib plus all of this file's definitions, which
* made resolving a large module O(calls x defs). The index answers each
* duplicated name with its first registration, as the scan did, so the
* resolution result is unchanged; post-walk additions (instance fields)
* stay visible through the registry's tail scan. The index lives in a
* scratch arena that dies with this call, not in the result arena. */
CBMArena idx_arena;
cbm_arena_init(&idx_arena);
reg.index_first_registered = true;
cbm_registry_finalize_into(&reg, &idx_arena);
PyLSPContext ctx;
py_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, &result->resolved_calls);
/* Let the resolver inject synthetic syntactic calls for operator/subscript
@@ -4926,7 +5099,11 @@ void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
}
}
py_memo_test_stage(&ctx, "raw");
py_lsp_process_file(&ctx, root);
if (ctx.eval_failure)
cbm_py_lsp_record_failure(result, ctx.eval_failure);
cbm_arena_destroy(&idx_arena);
}
/* ── Cross-file + batch ───────────────────────────────────────── */
@@ -5094,13 +5271,23 @@ static void py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRe
}
}
void cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
bool cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out,
CBMCallArray *synthetic_calls) {
return cbm_run_py_lsp_cross_status(arena, source, source_len, module_qn, defs, def_count,
import_names, import_qns, import_count, cached_tree, out,
synthetic_calls) == CBM_LSP_COMPLETE;
}
CBMLSPStatus cbm_run_py_lsp_cross_status(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns,
int import_count, TSTree *cached_tree,
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls) {
if (!arena || !source || source_len <= 0 || !out)
return;
return CBM_LSP_COMPLETE;
TSParser *parser = NULL;
TSTree *tree = cached_tree;
@@ -5108,13 +5295,13 @@ void cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
if (!tree) {
parser = ts_parser_new();
if (!parser)
return;
return CBM_LSP_COMPLETE;
ts_parser_set_language(parser, tree_sitter_python());
tree = cbm_parse_source(parser, source, (uint32_t)source_len, (TSParseOptions){0});
owns_tree = true;
if (!tree) {
ts_parser_delete(parser);
return;
return CBM_LSP_COMPLETE;
}
}
TSNode root = ts_tree_root_node(tree);
@@ -5141,6 +5328,7 @@ void cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
py_lsp_add_import(&ctx, import_names[i], import_qns[i]);
}
}
py_memo_test_stage(&ctx, "cross");
py_lsp_process_file(&ctx, root);
cbm_arena_destroy(&idx_arena);
@@ -5148,6 +5336,7 @@ void cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
ts_tree_delete(tree);
if (parser)
ts_parser_delete(parser);
return ctx.eval_failure;
}
/* ── Tier 2: pre-built per-language registry (mirrors Go pilot) ── */
@@ -5175,13 +5364,22 @@ CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
return reg;
}
void cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
bool cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMTypeRegistry *reg,
const char **import_names, const char **import_qns,
int import_count, TSTree *cached_tree,
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls) {
return cbm_run_py_lsp_cross_with_registry_status(
arena, source, source_len, module_qn, reg, import_names, import_qns, import_count,
cached_tree, out, synthetic_calls) == CBM_LSP_COMPLETE;
}
CBMLSPStatus cbm_run_py_lsp_cross_with_registry_status(
CBMArena *arena, const char *source, int source_len, const char *module_qn,
CBMTypeRegistry *reg, const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out, CBMCallArray *synthetic_calls) {
if (!arena || !source || source_len <= 0 || !out || !reg)
return;
return CBM_LSP_COMPLETE;
TSParser *parser = NULL;
TSTree *tree = cached_tree;
@@ -5189,13 +5387,13 @@ void cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int
if (!tree) {
parser = ts_parser_new();
if (!parser)
return;
return CBM_LSP_COMPLETE;
ts_parser_set_language(parser, tree_sitter_python());
tree = cbm_parse_source(parser, source, (uint32_t)source_len, (TSParseOptions){0});
owns_tree = true;
if (!tree) {
ts_parser_delete(parser);
return;
return CBM_LSP_COMPLETE;
}
}
TSNode root = ts_tree_root_node(tree);
@@ -5208,19 +5406,22 @@ void cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int
py_lsp_add_import(&ctx, import_names[i], import_qns[i]);
}
}
py_memo_test_stage(&ctx, "cross");
py_lsp_process_file(&ctx, root);
if (owns_tree && tree)
ts_tree_delete(tree);
if (parser)
ts_parser_delete(parser);
return ctx.eval_failure;
}
void cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_count,
bool cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_count,
CBMResolvedCallArray *out) {
if (!arena || !files || file_count <= 0 || !out)
return;
return true;
bool complete = true;
for (int f = 0; f < file_count; f++) {
CBMBatchPyLSPFile *file = &files[f];
memset(&out[f], 0, sizeof(CBMResolvedCallArray));
@@ -5233,9 +5434,10 @@ void cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_
CBMResolvedCallArray file_out;
memset(&file_out, 0, sizeof(file_out));
cbm_run_py_lsp_cross(&file_arena, file->source, file->source_len, file->module_qn,
file->defs, file->def_count, file->import_names, file->import_qns,
file->import_count, file->cached_tree, &file_out, NULL);
if (!cbm_run_py_lsp_cross(&file_arena, file->source, file->source_len, file->module_qn,
file->defs, file->def_count, file->import_names, file->import_qns,
file->import_count, file->cached_tree, &file_out, NULL))
complete = false;
if (file_out.count > 0) {
out[f].count = file_out.count;
@@ -5264,6 +5466,7 @@ void cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_
cbm_arena_destroy(&file_arena);
}
return complete;
}
/* ── Stdlib stub — Phase 10 replaces with auto-generated body ─── */
+36 -7
View File
@@ -95,14 +95,18 @@ typedef struct {
// py_eval_expr_type memoization + guards (issues #710/#720; mirrors
// c_eval_expr_type's guard design in c_lsp.c). All zero via memset —
// each file starts with a cold cache and a full budget.
// each file starts with a cold cache.
CBMPyTypeCacheEntry *type_cache; // open addressing, linear probe, arena-allocated
int type_cache_count; // occupied slots (kept < 75% of cap)
int type_cache_cap; // power-of-two capacity
uint32_t type_cache_gen; // bumped on every scope mutation (O(1) flush)
int eval_depth; // evaluator recursion depth (PY_LSP_MAX_EVAL_DEPTH)
int eval_steps; // per-file work budget used (PY_EVAL_MAX_STEPS_PER_FILE)
uint32_t eval_truncations; // depth/budget cutoff count — gates memo inserts
uint32_t eval_truncations; // depth-cap cutoff count — gates memo inserts
CBMLSPStatus eval_failure; // sticky: memo/depth failure makes this walk incomplete
#ifdef CBM_ENABLE_TEST_SEAMS
int test_memo_allocations_left;
int test_root_name_failure; // 1 = NULL, 2 = empty; only the root-name collection
#endif
// Sticky fail-closed guard for exact callable-value proof. If a required
// scope/name/overlay allocation fails, later semantic passes must leave
@@ -161,9 +165,28 @@ void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
// Phase 10 fills the body; Phase 2 ships a no-op so linkage works.
void cbm_python_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
// --- Cross-file LSP resolution ---
#define CBM_PY_LSP_DEPTH_ERROR "Python expression recursion depth limit exceeded"
#define CBM_PY_LSP_MEMO_ERROR "Python expression memo allocation or capacity failure"
void cbm_py_lsp_record_failure(CBMFileResult *result, CBMLSPStatus status);
#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_py_lsp_test_memo_fail_after(int successful_allocations);
void cbm_py_lsp_test_depth_fail(bool enabled);
const CBMType *cbm_py_lsp_test_eval(PyLSPContext *ctx, TSNode node);
void cbm_py_lsp_test_bind_external_classes(PyLSPContext *ctx, TSNode root);
#endif
void cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
// --- Cross-file LSP resolution ---
// Status variants distinguish memo/depth failure; bool wrappers report success.
// Existing no-op/parser exits retain their previous behavior.
CBMLSPStatus cbm_run_py_lsp_cross_status(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns,
int import_count,
TSTree *cached_tree, // NULL = parse internally
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
bool cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, // NULL = parse internally
@@ -173,7 +196,13 @@ void cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
* Filters all_defs to Python entries, builds + finalizes once. */
CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count);
void cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
CBMLSPStatus cbm_run_py_lsp_cross_with_registry_status(
CBMArena *arena, const char *source, int source_len, const char *module_qn,
CBMTypeRegistry *reg, // pre-built, finalized, READ-ONLY
const char **import_names, const char **import_qns, int import_count, TSTree *cached_tree,
CBMResolvedCallArray *out, CBMCallArray *synthetic_calls);
bool cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
const char *module_qn,
CBMTypeRegistry *reg, // pre-built, finalized, READ-ONLY
const char **import_names, const char **import_qns,
@@ -194,7 +223,7 @@ typedef struct {
int import_count;
} CBMBatchPyLSPFile;
void cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_count,
bool cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_count,
CBMResolvedCallArray *out);
#endif // CBM_LSP_PY_LSP_H
+146 -18
View File
@@ -1,5 +1,21 @@
#include "scope.h"
#include "lsp_work.h"
#include <string.h>
#include <limits.h>
#ifdef CBM_ENABLE_TEST_SEAMS
_Thread_local uint64_t cbm_lsp_work_steps = 0;
uint64_t cbm_lsp_work_take(void) {
uint64_t v = cbm_lsp_work_steps;
cbm_lsp_work_steps = 0;
return v;
}
#endif
enum {
SCOPE_INDEX_MIN_CAP = 64, /* first table: > 2x one chunk, a power of two */
SCOPE_INDEX_GROW = 2,
};
CBMScope* cbm_scope_push(CBMArena* a, CBMScope* current) {
CBMScope* scope = (CBMScope*)cbm_arena_alloc(a, sizeof(CBMScope));
@@ -33,6 +49,114 @@ static CBMScopeChunk* alloc_chunk(CBMScope* scope) {
return c;
}
static uint32_t scope_name_hash(const char *name) {
uint32_t h = 2166136261u; /* FNV-1a */
for (const unsigned char *p = (const unsigned char *)name; *p; p++) {
h ^= *p;
h *= 16777619u;
}
return h;
}
/* Place one binding into a table that has room (load kept below 1/2). */
static void scope_index_place(CBMVarBinding **slots, int cap, CBMVarBinding *b) {
uint32_t mask = (uint32_t)cap - 1;
uint32_t i = scope_name_hash(b->name) & mask;
while (slots[i]) {
i = (i + 1) & mask;
}
slots[i] = b;
}
/* (Re)build the frame's table at cap slots from its chunks. On allocation
* failure the frame drops its index and stays on the always-correct scan. */
static void scope_index_rebuild(CBMScope *scope, int cap) {
/* Recovery can start with many bindings and no index. Always size from
* the complete frame, never just from the previous table's capacity. */
if (scope->binding_count < 0 || scope->binding_count > INT_MAX / 2) {
memset(&scope->index, 0, sizeof(scope->index));
return;
}
if (cap < SCOPE_INDEX_MIN_CAP)
cap = SCOPE_INDEX_MIN_CAP;
while (cap < scope->binding_count * 2) {
if (cap > INT_MAX / SCOPE_INDEX_GROW) {
memset(&scope->index, 0, sizeof(scope->index));
return;
}
cap *= SCOPE_INDEX_GROW;
}
if ((size_t)cap > SIZE_MAX / sizeof(CBMVarBinding *)) {
memset(&scope->index, 0, sizeof(scope->index));
return;
}
CBMVarBinding **slots =
scope->arena
? (CBMVarBinding **)cbm_arena_alloc(scope->arena, (size_t)cap * sizeof(CBMVarBinding *))
: NULL;
if (!slots) {
memset(&scope->index, 0, sizeof(scope->index));
return;
}
memset(slots, 0, (size_t)cap * sizeof(CBMVarBinding *));
int count = 0;
for (CBMScopeChunk *c = scope->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name) {
scope_index_place(slots, cap, &c->bindings[i]);
count++;
}
}
}
scope->index.slots = slots;
scope->index.cap = cap;
scope->index.count = count;
}
/* Record a freshly appended binding in the frame's index, building the index
* the first time the frame outgrows one chunk. */
static void scope_index_note(CBMScope *scope, CBMVarBinding *b) {
if (scope->index.cap == 0) {
if (scope->binding_count > CBM_SCOPE_CHUNK_BINDINGS) {
scope_index_rebuild(scope, SCOPE_INDEX_MIN_CAP);
}
return;
}
if (scope->index.count >= scope->index.cap / 2) {
scope_index_rebuild(scope, scope->index.cap); /* includes b; sizes from binding_count */
return;
}
scope_index_place(scope->index.slots, scope->index.cap, b);
scope->index.count++;
}
/* The binding of name in one frame: hashed when the frame has an index,
* otherwise the linear chunk scan (small frames, or no index memory). */
static CBMVarBinding *scope_frame_find(const CBMScope *scope, const char *name) {
if (scope->index.cap > 0) {
uint32_t mask = (uint32_t)scope->index.cap - 1;
for (uint32_t i = scope_name_hash(name) & mask;; i = (i + 1) & mask) {
CBMVarBinding *b = scope->index.slots[i];
if (!b) {
return NULL;
}
CBM_LSP_WORK(1);
if (strcmp(b->name, name) == 0) {
return b;
}
}
}
for (CBMScopeChunk *c = scope->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
CBM_LSP_WORK(1);
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
return &c->bindings[i];
}
}
}
return NULL;
}
/* Returns false when the binding could NOT be recorded in THIS frame.
*
* The failure that matters is arena exhaustion in alloc_chunk: the old void
@@ -47,14 +171,11 @@ static bool cbm_scope_bind_value(CBMScope *scope, const char *name, const CBMTyp
if (!scope || !name) {
return false;
}
for (CBMScopeChunk* c = scope->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
c->bindings[i].type = type;
c->bindings[i].callable_qn = callable_qn;
return true;
}
}
CBMVarBinding *existing = scope_frame_find(scope, name);
if (existing) {
existing->type = type;
existing->callable_qn = callable_qn;
return true;
}
CBMScopeChunk* head = scope->chunks;
if (!head || head->used >= CBM_SCOPE_CHUNK_BINDINGS) {
@@ -63,10 +184,13 @@ static bool cbm_scope_bind_value(CBMScope *scope, const char *name, const CBMTyp
return false; /* arena exhausted: the shadow did NOT take effect */
}
}
head->bindings[head->used].name = name;
head->bindings[head->used].type = type;
head->bindings[head->used].callable_qn = callable_qn;
CBMVarBinding *b = &head->bindings[head->used];
b->name = name;
b->type = type;
b->callable_qn = callable_qn;
head->used++;
scope->binding_count++;
scope_index_note(scope, b);
return true;
}
@@ -88,6 +212,13 @@ bool cbm_scope_bind_callable_checked(CBMScope *scope, const char *name, const CB
return cbm_scope_bind_value(scope, name, type, callable_qn);
}
const CBMVarBinding *cbm_scope_lookup_local(const CBMScope *scope, const char *name) {
if (!scope || !name) {
return NULL;
}
return scope_frame_find(scope, name);
}
const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name) {
const CBMVarBinding *binding = cbm_scope_lookup_binding(scope, name);
if (binding)
@@ -109,13 +240,10 @@ bool cbm_scope_update_callable(CBMScope *scope, const char *name, const char *ca
return false;
}
for (CBMScope *s = scope; s != NULL; s = s->parent) {
for (CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
c->bindings[i].callable_qn = callable_qn;
return true;
}
}
CBMVarBinding *b = scope_frame_find(s, name);
if (b) {
b->callable_qn = callable_qn;
return true;
}
}
return false;
+28 -7
View File
@@ -26,27 +26,48 @@ typedef struct CBMScopeChunk {
struct CBMScopeChunk* next;
} CBMScopeChunk;
/* Name index for one frame (#1527). A frame that outgrows one chunk (a module
* scope binding thousands of classes and functions) is looked up through an
* open-addressing table of binding pointers instead of a linear chunk scan --
* the scan made every identifier lookup O(frame size) and a whole file
* O(n^2). Bindings never move (chunks are arena-allocated and only
* prepended) and a name is bound at most once per frame (rebinding updates
* in place), so the table maps each name to exactly the binding the linear
* scan would have found. The table is an accelerator only: when it cannot be
* allocated or grown the frame drops it and every lookup falls back to the
* scan, which is always correct. */
typedef struct {
CBMVarBinding **slots; // arena-owned; NULL = empty
int cap; // power of two, or 0 = no index (linear scan)
int count;
} CBMScopeIndex;
typedef struct CBMScope {
struct CBMScope* parent;
CBMScopeChunk* chunks;
CBMArena* arena; // owning arena, propagated to children at push time
int binding_count; // bindings recorded in THIS frame
CBMScopeIndex index; // built once binding_count exceeds one chunk
} CBMScope;
/* The binding of name in THIS frame only (no parent walk), or NULL. */
const CBMVarBinding *cbm_scope_lookup_local(const CBMScope *scope, const char *name);
/* Return the complete nearest binding in one scope-chain walk, or NULL when
* unbound. Keep this internal hot-path primitive inline: all language
* resolvers use cbm_scope_lookup(), while Python also consumes the complete
* record to avoid repeating the same linear scan. */
* record to avoid repeating the same walk. Each frame is probed through
* cbm_scope_lookup_local(), which uses the frame's hashed name index once the
* frame outgrows one chunk (#1527), so a lookup costs one probe per frame
* rather than a scan of every binding in scope. */
static inline const CBMVarBinding *cbm_scope_lookup_binding(const CBMScope *scope,
const char *name) {
if (!name)
return NULL;
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0)
return &c->bindings[i];
}
}
const CBMVarBinding *b = cbm_scope_lookup_local(s, name);
if (b)
return b;
}
return NULL;
}
+2 -6
View File
@@ -1392,12 +1392,8 @@ static const CBMScope *ts_scope_binding_owner(const CBMScope *scope, const char
return NULL;
}
for (const CBMScope *s = scope; s; s = s->parent) {
for (const CBMScopeChunk *chunk = s->chunks; chunk; chunk = chunk->next) {
for (int i = 0; i < chunk->used; i++) {
if (chunk->bindings[i].name && strcmp(chunk->bindings[i].name, name) == 0) {
return s;
}
}
if (cbm_scope_lookup_local(s, name)) {
return s;
}
}
return NULL;
+19 -2
View File
@@ -1,4 +1,5 @@
#include "type_registry.h"
#include "lsp_work.h"
#include <string.h>
#include <stdlib.h>
@@ -56,7 +57,12 @@ static void build_qn_index(CBMTypeRegistry *reg, CBMArena *idx_arena, bool for_f
for (int i = 0; i < bucket_count; i++)
buckets[i] = -1;
for (int i = 0; i < count; i++) {
/* Chains are built by prepending, so the entry inserted LAST is found
* first. Insert descending when the registry asked for first-registered
* answers (see index_first_registered). */
bool first = reg->index_first_registered;
for (int k = 0; k < count; k++) {
int i = first ? count - 1 - k : k;
const char *qn = for_funcs ? reg->funcs[i].qualified_name : reg->types[i].qualified_name;
if (!qn)
continue;
@@ -96,7 +102,9 @@ static void build_method_index(CBMTypeRegistry *reg, CBMArena *idx_arena) {
buckets[i] = -1;
int idx = 0;
for (int i = 0; i < reg->func_count; i++) {
bool first = reg->index_first_registered; /* see build_qn_index */
for (int k = 0; k < reg->func_count; k++) {
int i = first ? reg->func_count - 1 - k : k;
const CBMRegisteredFunc *f = &reg->funcs[i];
if (!f->receiver_type || !f->short_name)
continue;
@@ -779,6 +787,7 @@ static const CBMRegisteredFunc *lookup_method_self(const CBMTypeRegistry *reg,
int slot = (int)(h & (uint64_t)(reg->method_bucket_count - 1));
for (int idx = reg->method_buckets[slot]; idx >= 0;
idx = reg->method_entries[idx].next_index) {
CBM_LSP_WORK(1);
if (reg->method_entries[idx].hash != h)
continue;
const CBMRegisteredFunc *f = &reg->funcs[reg->method_entries[idx].payload_index];
@@ -792,6 +801,7 @@ static const CBMRegisteredFunc *lookup_method_self(const CBMTypeRegistry *reg,
* post-finalize additions stay visible instead of silently vanishing. */
for (int i = reg->func_qn_entry_count; i < reg->func_count; i++) {
const CBMRegisteredFunc *f = &reg->funcs[i];
CBM_LSP_WORK(1);
if (f->receiver_type && f->short_name && strcmp(f->receiver_type, receiver_qn) == 0 &&
strcmp(f->short_name, method_name) == 0) {
return f;
@@ -802,6 +812,7 @@ static const CBMRegisteredFunc *lookup_method_self(const CBMTypeRegistry *reg,
for (int i = 0; i < reg->func_count; i++) {
const CBMRegisteredFunc *f = &reg->funcs[i];
CBM_LSP_WORK(1);
if (f->receiver_type && f->short_name && strcmp(f->receiver_type, receiver_qn) == 0 &&
strcmp(f->short_name, method_name) == 0) {
return f;
@@ -829,6 +840,7 @@ static const CBMRegisteredType *lookup_type_self(const CBMTypeRegistry *reg,
int slot = (int)(h & (uint64_t)(reg->type_qn_bucket_count - 1));
for (int idx = reg->type_qn_buckets[slot]; idx >= 0;
idx = reg->type_qn_entries[idx].next_index) {
CBM_LSP_WORK(1);
if (reg->type_qn_entries[idx].hash != h)
continue;
int p = reg->type_qn_entries[idx].payload_index;
@@ -839,6 +851,7 @@ static const CBMRegisteredType *lookup_type_self(const CBMTypeRegistry *reg,
}
/* Tail-scan types added after finalize (see lookup_method_self). */
for (int i = reg->type_qn_entry_count; i < reg->type_count; i++) {
CBM_LSP_WORK(1);
if (reg->types[i].qualified_name &&
strcmp(reg->types[i].qualified_name, qualified_name) == 0) {
return &reg->types[i];
@@ -848,6 +861,7 @@ static const CBMRegisteredType *lookup_type_self(const CBMTypeRegistry *reg,
}
for (int i = 0; i < reg->type_count; i++) {
CBM_LSP_WORK(1);
if (strcmp(reg->types[i].qualified_name, qualified_name) == 0) {
return &reg->types[i];
}
@@ -873,6 +887,7 @@ static const CBMRegisteredFunc *lookup_func_self(const CBMTypeRegistry *reg,
int slot = (int)(h & (uint64_t)(reg->func_qn_bucket_count - 1));
for (int idx = reg->func_qn_buckets[slot]; idx >= 0;
idx = reg->func_qn_entries[idx].next_index) {
CBM_LSP_WORK(1);
if (reg->func_qn_entries[idx].hash != h)
continue;
int p = reg->func_qn_entries[idx].payload_index;
@@ -883,6 +898,7 @@ static const CBMRegisteredFunc *lookup_func_self(const CBMTypeRegistry *reg,
}
/* Tail-scan funcs added after finalize (see lookup_method_self). */
for (int i = reg->func_qn_entry_count; i < reg->func_count; i++) {
CBM_LSP_WORK(1);
if (reg->funcs[i].qualified_name &&
strcmp(reg->funcs[i].qualified_name, qualified_name) == 0) {
return &reg->funcs[i];
@@ -892,6 +908,7 @@ static const CBMRegisteredFunc *lookup_func_self(const CBMTypeRegistry *reg,
}
for (int i = 0; i < reg->func_count; i++) {
CBM_LSP_WORK(1);
if (strcmp(reg->funcs[i].qualified_name, qualified_name) == 0) {
return &reg->funcs[i];
}
+7
View File
@@ -152,6 +152,13 @@ typedef struct CBMTypeRegistry {
* every lookup linear-scans it -> O(files*defs) (the Linux-kernel full-index
* hang) plus a heap data race across workers. */
bool read_only;
// Set before finalize by a caller whose lookups used to run unfinalized
// (#1527): the exact-QN and method indexes then chain entries in
// registration order, so a duplicated name resolves to its FIRST
// registration -- the answer the linear scan gave. Without it the
// hashed chains answer with the LAST registration (the Tier-2 behavior,
// left unchanged).
bool index_first_registered;
} CBMTypeRegistry;
// Initialize a registry.
+28 -13
View File
@@ -1504,15 +1504,21 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
return; /* out may be partial: never append it */
}
} else {
cbm_run_c_lsp_cross(&scratch, source, source_len, module_qn, cpp_mode, defs, def_count,
NULL, NULL, 0, tree, &out);
CBMLSPStatus status =
cbm_run_c_lsp_cross_status(&scratch, source, source_len, module_qn, cpp_mode, defs,
def_count, NULL, NULL, 0, tree, &out);
if (status != CBM_LSP_COMPLETE)
cbm_c_lsp_record_failure(r, status);
}
break;
}
case CBM_LANG_PYTHON:
cbm_run_py_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names,
imp_qns, imp_count, tree, &out, &synthetic_calls);
break;
case CBM_LANG_PYTHON: {
CBMLSPStatus status = cbm_run_py_lsp_cross_status(&scratch, source, source_len, module_qn,
defs, def_count, imp_names, imp_qns,
imp_count, tree, &out, &synthetic_calls);
if (status != CBM_LSP_COMPLETE)
cbm_py_lsp_record_failure(r, status);
} break;
case CBM_LANG_PHP:
cbm_run_php_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names,
imp_qns, imp_count, tree, &out);
@@ -1663,10 +1669,13 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
&out);
used_prebuilt = true;
break;
case CBM_LANG_PYTHON:
cbm_run_py_lsp_cross_with_registry(&scratch, source, source_len, def_module, &overlay,
imp_keys, imp_vals, imp_count, result->cached_tree,
&out, &synthetic_calls);
case CBM_LANG_PYTHON: {
CBMLSPStatus status = cbm_run_py_lsp_cross_with_registry_status(
&scratch, source, source_len, def_module, &overlay, imp_keys, imp_vals, imp_count,
result->cached_tree, &out, &synthetic_calls);
if (status != CBM_LSP_COMPLETE)
cbm_py_lsp_record_failure(result, status);
}
used_prebuilt = true;
break;
case CBM_LANG_C:
@@ -1682,9 +1691,11 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
return; /* no append and no fallback on an invalid proof */
}
} else {
cbm_run_c_lsp_cross_with_registry(&scratch, source, source_len, def_module,
(lang != CBM_LANG_C), &overlay, imp_keys,
imp_vals, imp_count, result->cached_tree, &out);
CBMLSPStatus status = cbm_run_c_lsp_cross_with_registry_status(
&scratch, source, source_len, def_module, (lang != CBM_LANG_C), &overlay,
imp_keys, imp_vals, imp_count, result->cached_tree, &out);
if (status != CBM_LSP_COMPLETE)
cbm_c_lsp_record_failure(result, status);
}
used_prebuilt = true;
break;
@@ -2010,10 +2021,14 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *
* file, not to a stale extraction marker (the innocent-quarantine
* failure mode). */
cbm_index_mark_start(files[i].rel_path);
bool had_error = cache[i]->has_error;
cbm_pxc_dispatch_file(lang, cache[i], source, source_len, files[i].rel_path, def_modules[i],
&cross_registries, module_def_index, all_defs, def_count, imp_keys,
imp_vals, imp_count, NULL, NULL);
cbm_index_mark_done(files[i].rel_path);
if (!had_error && cache[i]->has_error && ctx->pipeline)
cbm_pipeline_add_file_error(ctx->pipeline, files[i].rel_path, cache[i]->error_msg,
"lsp_skipped");
per_lang_calls++;
processed++;
+32 -6
View File
@@ -77,6 +77,8 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 };
#include "pipeline/lsp_resolve.h"
#include "pipeline/laravel_routing.h"
#include "lsp/rust_cargo.h"
#include "lsp/c_lsp.h"
#include "lsp/py_lsp.h"
#include "helpers.h" /* cbm_kind_in_set_free_cache — per-worker-thread cache teardown */
#include "pipeline/worker_pool.h"
#include "foundation/compat.h"
@@ -1382,12 +1384,12 @@ static void extract_worker(int worker_id, void *ctx_ptr) {
snprintf(how_far, sizeof(how_far), "%u/%u nodes walked", result->walk_nodes_visited,
result->tree_nodes);
pp_err_add(errs, fi->rel_path, how_far, "walk_truncated");
} else if (result->lsp_skipped) {
/* Indexed, but without the per-file and cross-file LSP refinement:
* the same kind of hole from the other direction. Nothing in
* production sets this any more except a truncated walk (handled
* above) and the test seam — it is reported anyway, so that if
* something sets it again the gap arrives named, not silent. */
} else if (result->lsp_skipped && !result->has_error) {
/* has_error already recorded its precise cause above. Indexed, but without the per-file
* and cross-file LSP refinement: the same kind of hole from the other direction.
* Nothing in production sets this any more except a truncated walk (handled above) and
* the test seam — it is reported anyway, so that if something sets it again the gap
* arrives named, not silent. */
char size_text[CBM_SZ_64];
snprintf(size_text, sizeof(size_text), "%u nodes", result->tree_nodes);
pp_err_add(errs, fi->rel_path, size_text, "lsp_skipped");
@@ -2057,6 +2059,9 @@ typedef struct {
/* Counters for parallel.resolve.lsp_cross_done summary. */
_Atomic int lsp_cross_processed;
_Atomic int lsp_cross_skipped_no_source;
/* Each file is owned by one worker. The coordinator reports failures
* after join, without mutating the pipeline's error list concurrently. */
const char **lsp_failures;
/* Per-sub-phase timing (ns aggregated across workers) — surfaces
* exactly where parallel_resolve's wall time is spent so we stop
@@ -4176,11 +4181,20 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
* file around the resolve so a hang HERE is attributed to
* this file, not to a stale extraction marker. */
cbm_index_mark_start(rel);
bool had_error = result->has_error;
cbm_pxc_dispatch_file(lang, result, lsp_source, lsp_source_len, rel, def_module,
rc->cross_registries, rc->module_def_index, rc->all_defs,
rc->def_count, imp_keys, imp_vals, imp_count,
pp_rust_shared_registry_get, rc);
cbm_index_mark_done(rel);
if (!had_error && result->has_error && result->error_msg &&
(strcmp(result->error_msg, CBM_C_LSP_MEMO_ERROR) == 0 ||
strcmp(result->error_msg, CBM_PY_LSP_MEMO_ERROR) == 0 ||
strcmp(result->error_msg, CBM_C_LSP_DEPTH_ERROR) == 0 ||
strcmp(result->error_msg, CBM_PY_LSP_DEPTH_ERROR) == 0)) {
rc->lsp_failures[file_idx] = result->error_msg;
ws->errors++;
}
/* Free the on-demand re-read (no-op when source was retained). */
free_source(lsp_source_owned);
/* Contract: cbm_slab_reclaim() requires the thread parser to be
@@ -4317,6 +4331,11 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
return CBM_NOT_FOUND;
}
memset(workers, 0, (size_t)worker_count * sizeof(resolve_worker_state_t));
const char **lsp_failures = calloc((size_t)file_count, sizeof(*lsp_failures));
if (!lsp_failures) {
cbm_aligned_free(workers);
return CBM_NOT_FOUND;
}
bool have_rust = false;
for (int i = 0; i < file_count; i++) {
@@ -4357,6 +4376,7 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
.module_def_index = module_def_index,
.cross_registries = cross_registries,
.rust_manifest = rust_manifest_ptr,
.lsp_failures = lsp_failures,
};
atomic_init(&rc.next_file_idx, 0);
atomic_init(&rc.lsp_cross_processed, 0);
@@ -4374,6 +4394,12 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
cbm_scale_end(&rc.scale);
CBM_PROF_END_N("parallel_resolve", "1_dispatch_workers_parallel", t_resolve_dispatch,
file_count);
for (int i = 0; i < file_count; i++) {
if (lsp_failures[i] && ctx->pipeline)
cbm_pipeline_add_file_error(ctx->pipeline, files[i].rel_path, lsp_failures[i],
"lsp_skipped");
}
free(lsp_failures);
/* Workers joined: the shared Rust registry (if built) is no longer read.
* Free its dedicated arena + the lock (registry was self-contained: it strdup'd
* all QNs, so freeing all_defs afterward is safe). */
+477
View File
@@ -19,6 +19,7 @@
* - DLL patterns, SFINAE, placement new
*/
#include "test_framework.h"
#include "../src/foundation/compat.h"
#include "cbm.h"
#include "lsp/c_lsp.h"
#include "lsp/py_lsp.h"
@@ -26,7 +27,9 @@
#include "lsp/ts_lsp.h"
#include "lsp/go_lsp.h"
#include "lsp/type_registry.h"
#include "lsp/lsp_work.h"
#include "../src/pipeline/lsp_resolve.h"
#include "../src/pipeline/pass_lsp_cross.h"
#include "arena.h"
#include "preprocessor.h"
#include <stdio.h>
@@ -16535,7 +16538,479 @@ TEST(clsp_preprocessed_destructor_rewrite_respects_origin_during_rewrite) {
PASS();
}
/* ── #1527: no per-file eval budget; cost fixed at the root ────────────── */
/* Resolved rows that name a real target (not an lsp_unresolved marker) whose
* callee QN contains sub. */
static int count_real_resolved(const CBMFileResult *r, const char *sub) {
int n = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (rc->callee_qn && strstr(rc->callee_qn, sub) && rc->confidence > 0.0f &&
!(rc->strategy && strcmp(rc->strategy, "lsp_unresolved") == 0))
n++;
}
return n;
}
/* A versionbits-style fluent chain (the #323 hang): every link returns
* Tester&, and the evaluator re-evaluates a call's receiver for template
* substitution, so without a memo an N-link chain costs 2^N evaluations. The
* per-file step cap that used to contain it is gone; the per-evaluation memo
* must keep the work polynomial AND every link must resolve. */
static char *clsp_chain_source(int links, int stmts) {
size_t sz = 512 + (size_t)stmts * (32 + (size_t)links * 40);
char *src = malloc(sz);
if (!src)
return NULL;
size_t pos = (size_t)snprintf(src, sz,
"struct Tester {\n"
" Tester& Mine(int h, int t, int v);\n"
" Tester& TestDefined();\n"
" Tester& TestStateSinceHeight(int h);\n"
" Tester& Reset();\n"
"};\n"
"void run() {\n"
" Tester t;\n");
for (int s = 0; s < stmts; s++) {
pos += (size_t)snprintf(src + pos, sz - pos, " t.Reset()");
for (int l = 0; l < links; l++) {
if (l % 3 == 0)
pos += (size_t)snprintf(src + pos, sz - pos, ".Mine(%d, %d, 0x100)", l, l);
else if (l % 3 == 1)
pos += (size_t)snprintf(src + pos, sz - pos, ".TestDefined()");
else
pos += (size_t)snprintf(src + pos, sz - pos, ".TestStateSinceHeight(%d)", l);
}
pos += (size_t)snprintf(src + pos, sz - pos, ";\n");
}
snprintf(src + pos, sz - pos, "}\n");
return src;
}
TEST(clsp_issue1527_fluent_chain_work_is_polynomial) {
enum { LINKS = 14, STMTS = 3 };
char *src = clsp_chain_source(LINKS, STMTS);
ASSERT_NOT_NULL(src);
(void)cbm_lsp_work_take();
CBMFileResult *r = extract_cpp(src);
uint64_t work = cbm_lsp_work_take();
free(src);
ASSERT_NOT_NULL(r);
printf(" chain %dx%d: work=%llu\n", LINKS, STMTS, (unsigned long long)work);
/* Every link of every statement resolves to its Tester method. */
ASSERT_EQ(count_real_resolved(r, ".Tester."), STMTS * (LINKS + 1));
/* Memoized: ~LINKS^2 evaluations per statement (each call node starts a
* fresh outermost evaluation of its receiver chain) -- ~1.5k in total.
* Unmemoized: ~2^LINKS per call node, ~100k in total. */
ASSERT(work < 10000);
cbm_free_result(r);
PASS();
}
/* Deterministic complexity gate for the C/C++ resolver (O9: no clock): 4x the
* definitions and calls must cost ~4x the counted work (scope-frame probes,
* registry lookup probes, enclosing-definition candidates, real
* evaluations). Methods are DEFINED (out of class), so they are registered
* before the registry is indexed. Declaration-only methods are registered by
* the walk itself, after indexing, and are still found by a linear tail scan
* -- a separate, known quadratic (see the #1527 follow-up), not measured here. */
static uint64_t clsp_member_calls_work(int n, int *out_real) {
size_t sz = (size_t)n * 128 + 512;
char *src = malloc(sz);
if (!src)
return 0;
size_t pos = (size_t)snprintf(src, sz, "struct S {\n");
for (int i = 0; i < n; i++)
pos += (size_t)snprintf(src + pos, sz - pos, " S& m%d();\n", i);
pos += (size_t)snprintf(src + pos, sz - pos, "};\n");
for (int i = 0; i < n; i++)
pos += (size_t)snprintf(src + pos, sz - pos, "S& S::m%d() { return *this; }\n", i);
pos += (size_t)snprintf(src + pos, sz - pos, "void use() {\n S s;\n");
for (int i = 0; i < n; i++)
pos += (size_t)snprintf(src + pos, sz - pos, " s.m%d().m%d();\n", i, (i + 1) % n);
snprintf(src + pos, sz - pos, "}\n");
(void)cbm_lsp_work_take();
CBMFileResult *r = extract_cpp(src);
uint64_t work = cbm_lsp_work_take();
free(src);
*out_real = r ? count_real_resolved(r, ".S.m") : -1;
if (r)
cbm_free_result(r);
return work;
}
TEST(clsp_issue1527_scale_work_is_linear) {
int real1 = 0, real4 = 0;
uint64_t w1 = clsp_member_calls_work(1000, &real1);
uint64_t w4 = clsp_member_calls_work(4000, &real4);
double ratio = w1 ? (double)w4 / (double)w1 : 0.0;
printf(" work: 1000=%llu (real=%d) 4000=%llu (real=%d) ratio=%.2fx\n",
(unsigned long long)w1, real1, (unsigned long long)w4, real4, ratio);
ASSERT_EQ(real1, 2 * 1000);
ASSERT_EQ(real4, 2 * 4000);
ASSERT_GT(w1, 0);
ASSERT(ratio < 5.0);
PASS();
}
/* A reachable post-allocation-failure state, pinned without memory pressure.
* The old recovery path returned a full table here. Check capacity BEFORE
* any missing-name lookup, and free the arena before the predicted RED. */
TEST(scope_issue1527_recovery_sizes_from_all_bindings) {
CBMArena a;
cbm_arena_init(&a);
CBMScope *scope = cbm_scope_push(&a, NULL);
char names[64][24];
bool setup_ok = scope != NULL;
for (int i = 0; i < 63 && setup_ok; i++) {
snprintf(names[i], sizeof(names[i]), "binding_%d", i);
setup_ok = cbm_scope_bind_callable_checked(scope, names[i], cbm_type_unknown(), "old");
}
const CBMVarBinding *stable = setup_ok ? cbm_scope_lookup_local(scope, names[7]) : NULL;
if (setup_ok)
memset(&scope->index, 0, sizeof(scope->index));
snprintf(names[63], sizeof(names[63]), "binding_63");
bool bound =
setup_ok && cbm_scope_bind_callable_checked(scope, names[63], cbm_type_unknown(), "old");
bool spare = bound && scope->binding_count == 64 && scope->index.cap >= 128;
bool preserved = false;
if (spare) {
preserved = cbm_scope_lookup_local(scope, "absent") == NULL;
for (int i = 0; i < 64; i++)
preserved = preserved && cbm_scope_lookup_local(scope, names[i]) != NULL;
preserved = preserved &&
cbm_scope_bind_callable_checked(scope, names[7], cbm_type_unknown(), "new") &&
cbm_scope_lookup_local(scope, names[7]) == stable && stable->callable_qn &&
strcmp(stable->callable_qn, "new") == 0 && scope->binding_count == 64;
CBMScope *child = cbm_scope_push(&a, scope);
preserved = preserved && child &&
cbm_scope_bind_callable_checked(child, names[7], cbm_type_unknown(), "child");
const CBMVarBinding *shadow = child ? cbm_scope_lookup_binding(child, names[7]) : NULL;
preserved = preserved && shadow && strcmp(shadow->callable_qn, "child") == 0 &&
strcmp(stable->callable_qn, "new") == 0;
}
cbm_arena_destroy(&a);
ASSERT_TRUE(setup_ok);
ASSERT_TRUE(bound);
ASSERT_TRUE(spare);
ASSERT_TRUE(preserved);
PASS();
}
/* Initial allocation, second allocation, and later growth fail independently
* of the arena. The failure must be explicit, not an uncached exponential walk. */
TEST(clsp_issue1527_memo_allocation_failure_is_reported) {
char *src = clsp_chain_source(40, 1);
ASSERT_NOT_NULL(src);
bool failures_reported = true;
for (int successful_allocations = 0; successful_allocations <= 2; successful_allocations++) {
(void)cbm_c_lsp_test_walks_take();
cbm_c_lsp_test_memo_fail_after(successful_allocations);
CBMFileResult *r = extract_cpp(src);
uint32_t walks = cbm_c_lsp_test_walks_take();
cbm_c_lsp_test_memo_fail_after(-1);
failures_reported = failures_reported && r && r->has_error && r->lsp_skipped &&
r->error_msg && strcmp(r->error_msg, CBM_C_LSP_MEMO_ERROR) == 0 &&
r->resolved_calls.count == 0 && walks == 1;
if (r) {
/* The allocation-free diagnostic also survives result compaction. */
cbm_result_compact(r);
failures_reported = failures_reported && r->error_msg &&
strcmp(r->error_msg, CBM_C_LSP_MEMO_ERROR) == 0;
cbm_free_result(r);
}
}
CBMFileResult *fresh = extract_cpp(src);
bool recovered = fresh && !fresh->has_error && !fresh->lsp_skipped &&
count_real_resolved(fresh, ".Tester.") == 41;
if (fresh)
cbm_free_result(fresh);
free(src);
ASSERT_TRUE(failures_reported);
ASSERT_TRUE(recovered);
PASS();
}
extern const TSLanguage *tree_sitter_cpp(void);
TEST(clsp_issue1527_memo_and_depth_failures_are_sticky) {
const char *src = "void f() { x; }";
TSParser *parser = ts_parser_new();
ASSERT_NOT_NULL(parser);
ts_parser_set_language(parser, tree_sitter_cpp());
TSTree *tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)strlen(src));
if (!tree) {
ts_parser_delete(parser);
FAIL("failed to parse memo fixture");
}
TSNode fn = ts_node_named_child(ts_tree_root_node(tree), 0);
TSNode body = ts_node_child_by_field_name(fn, "body", 4);
TSNode expr = ts_node_named_child(ts_node_named_child(body, 0), 0);
bool identifier = !ts_node_is_null(expr) && strcmp(ts_node_type(expr), "identifier") == 0;
CBMArena a;
cbm_arena_init(&a);
CBMTypeRegistry reg;
cbm_registry_init(&reg, &a);
CBMResolvedCallArray out = {0};
CLSPContext ctx;
c_lsp_init(&ctx, &a, src, (int)strlen(src), &reg, "test", true, &out);
const CBMType *first = cbm_type_named(&a, "test.First");
const CBMType *second = cbm_type_named(&a, "test.Second");
cbm_scope_bind(ctx.current_scope, "x", first);
ctx.eval_depth = 257;
bool depth_unknown = cbm_type_is_unknown(c_eval_expr_type(&ctx, expr)) &&
ctx.eval_failure == CBM_LSP_DEPTH_EXCEEDED;
ctx.eval_depth = 0;
(void)cbm_lsp_work_take();
bool depth_stopped = cbm_type_is_unknown(c_eval_expr_type(&ctx, expr));
depth_stopped = depth_stopped && cbm_lsp_work_take() == 0;
/* A depth-truncated walk is incomplete; only a fresh walk may resume. */
c_lsp_init(&ctx, &a, src, (int)strlen(src), &reg, "test", true, &out);
cbm_scope_bind(ctx.current_scope, "x", first);
bool shallow_ok = c_eval_expr_type(&ctx, expr) == first && !ctx.eval_failure;
cbm_scope_bind(ctx.current_scope, "x", second);
bool rebound = c_eval_expr_type(&ctx, expr) == second;
/* Force a new memo's allocation to fail; old slots remain arena-owned. */
ctx.eval_memo_cap = ctx.eval_memo_count = 0;
ctx.eval_memo = NULL;
ctx.eval_memo_used = NULL;
ctx.test_memo_allocations_left = 0;
bool failed = cbm_type_is_unknown(c_eval_expr_type(&ctx, expr)) && ctx.eval_failure;
(void)cbm_lsp_work_take();
bool stopped = true;
for (int i = 0; i < 20; i++)
stopped = stopped && cbm_type_is_unknown(c_eval_expr_type(&ctx, expr));
uint64_t later_work = cbm_lsp_work_take();
cbm_arena_destroy(&a);
ts_tree_delete(tree);
ts_parser_delete(parser);
ASSERT_TRUE(identifier);
ASSERT_TRUE(depth_unknown);
ASSERT_TRUE(depth_stopped);
ASSERT_TRUE(shallow_ok);
ASSERT_TRUE(rebound);
ASSERT_TRUE(failed);
ASSERT_TRUE(stopped);
ASSERT_EQ(later_work, 0);
PASS();
}
TEST(clsp_issue1527_preprocessed_memo_failure_is_reported) {
const char *key = "CBM_TEST_C_LSP_MEMO_FAIL_STAGE";
const char *old = getenv(key);
char *saved = old ? strdup(old) : NULL;
if (old && !saved)
FAIL("failed to save memo seam");
int set_rc = cbm_setenv(key, "preprocessed", 1);
(void)cbm_c_lsp_test_walks_take();
CBMFileResult *r = set_rc == 0 ? extract_cpp("struct S { int m() { return 1; } };\n"
"#define INVOKE(s) s.m()\n"
"void run() { S s; INVOKE(s); }\n")
: NULL;
uint32_t walks = cbm_c_lsp_test_walks_take();
bool reported = r && r->has_error && r->lsp_skipped && r->error_msg &&
strcmp(r->error_msg, CBM_C_LSP_MEMO_ERROR) == 0 && walks == 2;
if (r)
cbm_free_result(r);
int restore_rc = saved ? cbm_setenv(key, saved, 1) : cbm_unsetenv(key);
free(saved);
ASSERT_EQ(set_rc, 0);
ASSERT_EQ(restore_rc, 0);
ASSERT_TRUE(reported);
PASS();
}
TEST(clsp_issue1527_preprocessed_depth_failure_is_reported) {
const char *key = "CBM_TEST_C_LSP_DEPTH_FAIL_STAGE";
const char *old = getenv(key);
char *saved = old ? strdup(old) : NULL;
if (old && !saved)
FAIL("failed to save memo seam");
int set_rc = cbm_setenv(key, "preprocessed", 1);
(void)cbm_c_lsp_test_walks_take();
CBMFileResult *r = set_rc == 0 ? extract_cpp("struct S { int m() { return 1; } };\n"
"#define INVOKE(s) s.m()\n"
"void run() { S s; INVOKE(s); }\n")
: NULL;
uint32_t walks = cbm_c_lsp_test_walks_take();
bool reported = r && r->has_error && r->lsp_skipped && r->error_msg &&
strcmp(r->error_msg, CBM_C_LSP_DEPTH_ERROR) == 0 && walks == 2;
if (r)
cbm_free_result(r);
int restore_rc = saved ? cbm_setenv(key, saved, 1) : cbm_unsetenv(key);
free(saved);
ASSERT_EQ(set_rc, 0);
ASSERT_EQ(restore_rc, 0);
ASSERT_TRUE(reported);
PASS();
}
TEST(clsp_issue1527_cross_file_memo_failures_propagate) {
const char *src = "struct S { int m() { return 1; } }; void run() { S s; s.m(); }";
CBMLSPDef defs[] = {
{.qualified_name = "test.S", .short_name = "S", .label = "Class", .lang = CBM_LANG_CPP},
{.qualified_name = "test.S.m",
.short_name = "m",
.label = "Method",
.receiver_type = "test.S",
.return_types = "int",
.lang = CBM_LANG_CPP},
};
bool failed[3] = {false}, recovered[3] = {false};
bool shared_ready = true;
for (int mode = 0; mode < 3; mode++) {
CBMArena a;
cbm_arena_init(&a);
CBMTypeRegistry reg;
cbm_registry_init(&reg, &a);
CBMTypeRegistry *shared = cbm_c_build_cross_registry(&a, defs, 2);
shared_ready = shared_ready && shared && shared->read_only;
reg.fallback = shared; /* the production prebuilt path uses a writable overlay */
CBMResolvedCallArray out = {0};
CBMBatchCLSPFile file = {.source = src,
.source_len = (int)strlen(src),
.module_qn = "test",
.cpp_mode = true,
.defs = defs,
.def_count = 2};
cbm_c_lsp_test_memo_fail_after(0);
bool ok = mode == 0 ? cbm_run_c_lsp_cross(&a, src, (int)strlen(src), "test", true, defs, 2,
NULL, NULL, 0, NULL, &out)
: mode == 1
? cbm_run_c_lsp_cross_with_registry(&a, src, (int)strlen(src), "test", true,
&reg, NULL, NULL, 0, NULL, &out)
: cbm_batch_c_lsp_cross(&a, &file, 1, &out);
cbm_c_lsp_test_memo_fail_after(-1);
failed[mode] = !ok && out.count == 0;
ok = mode == 0 ? cbm_run_c_lsp_cross(&a, src, (int)strlen(src), "test", true, defs, 2, NULL,
NULL, 0, NULL, &out)
: mode == 1 ? cbm_run_c_lsp_cross_with_registry(&a, src, (int)strlen(src), "test",
true, &reg, NULL, NULL, 0, NULL, &out)
: cbm_batch_c_lsp_cross(&a, &file, 1, &out);
recovered[mode] = ok && out.count > 0;
cbm_arena_destroy(&a);
}
ASSERT_TRUE(shared_ready);
for (int i = 0; i < 3; i++) {
ASSERT_TRUE(failed[i]);
ASSERT_TRUE(recovered[i]);
}
PASS();
}
TEST(registry_issue1527_index_preserves_registration_order) {
CBMArena a;
cbm_arena_init(&a);
CBMTypeRegistry reg;
cbm_registry_init(&reg, &a);
CBMRegisteredType t = {.qualified_name = "test.S", .short_name = "S"};
cbm_registry_add_type(&reg, t);
cbm_registry_add_type(&reg, t);
CBMRegisteredFunc f = {
.qualified_name = "test.S.m", .short_name = "m", .receiver_type = "test.S"};
cbm_registry_add_func(&reg, f);
cbm_registry_add_func(&reg, f);
bool linear_first = cbm_registry_lookup_type(&reg, "test.S") == &reg.types[0] &&
cbm_registry_lookup_func(&reg, "test.S.m") == &reg.funcs[0] &&
cbm_registry_lookup_method(&reg, "test.S", "m") == &reg.funcs[0];
reg.index_first_registered = true;
cbm_registry_finalize(&reg);
bool indexed_first = cbm_registry_lookup_type(&reg, "test.S") == &reg.types[0] &&
cbm_registry_lookup_func(&reg, "test.S.m") == &reg.funcs[0] &&
cbm_registry_lookup_method(&reg, "test.S", "m") == &reg.funcs[0];
reg.index_first_registered = false;
cbm_registry_finalize(&reg);
bool default_last = cbm_registry_lookup_type(&reg, "test.S") == &reg.types[1] &&
cbm_registry_lookup_func(&reg, "test.S.m") == &reg.funcs[1] &&
cbm_registry_lookup_method(&reg, "test.S", "m") == &reg.funcs[1];
f.qualified_name = "test.S.tail";
f.short_name = "tail";
cbm_registry_add_func(&reg, f);
bool tail = cbm_registry_lookup_func(&reg, "test.S.tail") == &reg.funcs[2] &&
cbm_registry_lookup_method(&reg, "test.S", "tail") == &reg.funcs[2];
cbm_arena_destroy(&a);
ASSERT_TRUE(linear_first);
ASSERT_TRUE(indexed_first);
ASSERT_TRUE(default_last);
ASSERT_TRUE(tail);
PASS();
}
TEST(clsp_issue1527_depth_failure_reports_all_entry_points) {
const char *src = "struct S { int m() { return 1; } }; void run() { S s; s.m(); }";
CBMLSPDef defs[] = {
{.qualified_name = "test.S", .short_name = "S", .label = "Class", .lang = CBM_LANG_CPP},
{.qualified_name = "test.S.m",
.short_name = "m",
.label = "Method",
.receiver_type = "test.S",
.return_types = "int",
.lang = CBM_LANG_CPP},
};
cbm_c_lsp_test_depth_fail(true);
CBMFileResult *raw = extract_cpp(src);
bool correct = raw && raw->has_error && raw->lsp_skipped && raw->error_msg &&
strcmp(raw->error_msg, CBM_C_LSP_DEPTH_ERROR) == 0 &&
raw->resolved_calls.count == 0;
if (raw)
cbm_free_result(raw);
for (int mode = 0; mode < 3; mode++) {
CBMArena arena, shared;
cbm_arena_init(&arena);
cbm_arena_init(&shared);
CBMTypeRegistry *base = mode == 1 ? cbm_c_build_cross_registry(&shared, defs, 2) : NULL;
CBMTypeRegistry overlay;
cbm_registry_init(&overlay, &arena);
overlay.fallback = base;
CBMResolvedCallArray out = {0};
CBMBatchCLSPFile file = {.source = src,
.source_len = (int)strlen(src),
.module_qn = "test",
.defs = defs,
.def_count = 2,
.cpp_mode = true};
if (mode == 2) {
correct = !cbm_batch_c_lsp_cross(&arena, &file, 1, &out) && correct;
} else {
CBMLSPStatus status =
mode == 0 ? cbm_run_c_lsp_cross_status(&arena, src, (int)strlen(src), "test", true,
defs, 2, NULL, NULL, 0, NULL, &out)
: cbm_run_c_lsp_cross_with_registry_status(&arena, src, (int)strlen(src),
"test", true, &overlay, NULL,
NULL, 0, NULL, &out);
correct = correct && status == CBM_LSP_DEPTH_EXCEEDED;
}
correct = correct && out.count == 0;
cbm_arena_destroy(&arena);
cbm_arena_destroy(&shared);
}
CBMFileResult fallback = {0};
cbm_arena_init(&fallback.arena);
cbm_pxc_run_one(CBM_LANG_CPP, &fallback, src, (int)strlen(src), "test", defs, 2, NULL, NULL, 0);
correct = correct && fallback.has_error && fallback.lsp_skipped && fallback.error_msg &&
strcmp(fallback.error_msg, CBM_C_LSP_DEPTH_ERROR) == 0 &&
fallback.resolved_calls.count == 0;
cbm_arena_destroy(&fallback.arena);
cbm_c_lsp_test_depth_fail(false);
CBMFileResult *fresh = extract_cpp(src);
bool recovered = fresh && !fresh->has_error && fresh->resolved_calls.count > 0;
if (fresh)
cbm_free_result(fresh);
ASSERT_TRUE(correct);
ASSERT_TRUE(recovered);
PASS();
}
SUITE(c_lsp) {
RUN_TEST(clsp_issue1527_depth_failure_reports_all_entry_points);
RUN_TEST(scope_issue1527_recovery_sizes_from_all_bindings);
RUN_TEST(clsp_issue1527_memo_allocation_failure_is_reported);
RUN_TEST(clsp_issue1527_memo_and_depth_failures_are_sticky);
RUN_TEST(clsp_issue1527_preprocessed_memo_failure_is_reported);
RUN_TEST(clsp_issue1527_preprocessed_depth_failure_is_reported);
RUN_TEST(registry_issue1527_index_preserves_registration_order);
RUN_TEST(clsp_issue1527_cross_file_memo_failures_propagate);
RUN_TEST(clsp_c_reassigned_function_pointer_calls_join_exact_occurrences);
RUN_TEST(clsp_c_nested_function_pointer_shadow_restores_outer_target);
RUN_TEST(clsp_cpp_repeated_same_leaf_calls_join_exact_occurrences);
@@ -17301,4 +17776,6 @@ SUITE(c_lsp) {
RUN_TEST(clsp_dll_multiple_functions);
RUN_TEST(clsp_dll_func_ptr_typedef);
RUN_TEST(clsp_easy_win_sfinaeconditional_return);
RUN_TEST(clsp_issue1527_fluent_chain_work_is_polynomial);
RUN_TEST(clsp_issue1527_scale_work_is_linear);
}
+46
View File
@@ -10942,6 +10942,51 @@ TEST(extract_deep_argument_nesting_completes) {
PASS();
}
/* #1527: the call-context metrics find each call's innermost enclosing
* Function/Method with a line sweep instead of scanning every def per call.
* The sweep must give the scan's exact answer -- smallest span, ties to the
* lowest def index -- on nested, overlapping, same-line, malformed
* (end < start) and unnamed/non-callable ranges. Fixed-seed generator:
* deterministic, many shapes. */
static uint32_t enc_rand(uint32_t *state) {
*state = (*state * 1103515245u) + 12345u;
return (*state >> 16) & 0x7fffu;
}
TEST(extract_enclosing_sweep_matches_scan_issue1527) {
static const char *const labels[] = {"Function", "Method", "Class", "Variable"};
uint32_t seed = 1527;
for (int round = 0; round < 40; round++) {
CBMArena a;
cbm_arena_init(&a);
CBMFileResult r;
memset(&r, 0, sizeof(r));
int ndefs = round == 0 ? 0 : (int)(enc_rand(&seed) % 300);
int ncalls = (int)(enc_rand(&seed) % 1500) + 1;
int max_line = (int)(enc_rand(&seed) % 400) + 5;
for (int i = 0; i < ndefs; i++) {
CBMDefinition d;
memset(&d, 0, sizeof(d));
d.name = (enc_rand(&seed) % 10) == 0 ? NULL : "f";
d.label = labels[enc_rand(&seed) % 4];
d.start_line = 1 + (enc_rand(&seed) % (uint32_t)max_line);
int len = (int)(enc_rand(&seed) % 60) - 3; /* a few malformed end < start */
d.end_line = (uint32_t)((int)d.start_line + len > 0 ? (int)d.start_line + len : 0);
cbm_defs_push(&r.defs, &a, d);
}
for (int i = 0; i < ncalls; i++) {
CBMCall c;
memset(&c, 0, sizeof(c));
c.callee_name = "g";
c.start_line = (int)(enc_rand(&seed) % (uint32_t)(max_line + 10)) - 2;
cbm_calls_push(&r.calls, &a, c);
}
ASSERT_EQ(cbm_test_enclosing_sweep_mismatches(&r), 0);
cbm_arena_destroy(&a);
}
PASS();
}
SUITE(extraction) {
RUN_TEST(extract_compact_keeps_every_field_and_shrinks_the_arena);
RUN_TEST(extract_compact_is_idempotent_and_survives_empty_results);
@@ -11427,6 +11472,7 @@ SUITE(extraction) {
RUN_TEST(json_toplevel_description_promoted_to_module_issue519);
RUN_TEST(config_description_only_at_top_level_issue519);
RUN_TEST(non_config_language_module_has_no_promoted_description_issue519);
RUN_TEST(extract_enclosing_sweep_matches_scan_issue1527);
/* Syntax-tree walkers spend no stack per nesting level */
RUN_TEST(extract_deep_callee_chain_completes);
+182
View File
@@ -10,7 +10,10 @@
#include "test_helpers.h"
#include "foundation/mem.h" // cbm_mem_init/budget (back-pressure futile-nap test)
#include "pipeline/pipeline.h"
#include "lsp/c_lsp.h"
#include "lsp/py_lsp.h"
#include "pipeline/lsp_surface.h"
#include "pipeline/pass_lsp_cross.h"
#include "pipeline/pipeline_internal.h"
#include "pipeline/artifact.h"
#include "store/store.h"
@@ -10517,6 +10520,168 @@ static void teardown_usages_repo(void) {
g_usages_tmpdir[0] = '\0';
}
/* Real cross-file memo failure, after successful extraction. The failure
* must reach the public per-file error list on both production drivers. */
static int pipeline_lsp_failure_case(bool parallel, const char *stage, bool python, bool depth) {
const char *src = "struct S { int m() { return 1; } };\n"
"void run() { S s; s.m(); }\n";
if (python)
src = "class S:\n def m(self):\n return 1\ndef run():\n s = S()\n s.m()\n";
const char *filename = python ? "memo.py" : "memo.cpp";
if (setup_usages_repo(filename, src, NULL, NULL) != 0)
FAIL("failed to create memo fixture");
char path[512];
if (parallel) {
for (int i = 0; i < 51; i++) {
snprintf(path, sizeof(path), "%s/memo_pad_%02d.%s", g_usages_tmpdir, i,
python ? "py" : "cpp");
if (th_write_file(path, python ? "# inert parallel routing fixture\n"
: "// inert parallel routing fixture\n") != 0) {
teardown_usages_repo();
FAIL("failed to create parallel routing fixture");
}
}
}
const char *keys[] = {
"CBM_WORKERS", "CBM_INDEX_SINGLE_THREAD",
depth ? (python ? "CBM_TEST_PY_LSP_DEPTH_FAIL_STAGE" : "CBM_TEST_C_LSP_DEPTH_FAIL_STAGE")
: (python ? "CBM_TEST_PY_LSP_MEMO_FAIL_STAGE" : "CBM_TEST_C_LSP_MEMO_FAIL_STAGE")};
char *saved[3] = {0};
bool env_saved = true;
for (int i = 0; i < 3; i++) {
const char *old = getenv(keys[i]);
saved[i] = old ? strdup(old) : NULL;
env_saved = env_saved && (!old || saved[i]);
}
if (!env_saved) {
for (int i = 0; i < 3; i++)
free(saved[i]);
teardown_usages_repo();
FAIL("failed to save memo fixture environment");
}
int set_rc = cbm_setenv(keys[0], parallel ? "4" : "1", 1);
set_rc |= parallel ? cbm_unsetenv(keys[1]) : cbm_setenv(keys[1], "1", 1);
set_rc |= cbm_setenv(keys[2], stage, 1);
snprintf(path, sizeof(path), "%s/memo.db", g_usages_tmpdir);
cbm_pipeline_t *p = set_rc == 0 ? cbm_pipeline_new(g_usages_tmpdir, path, CBM_MODE_FULL) : NULL;
int run_rc = p ? cbm_pipeline_run(p) : -1;
cbm_file_error_t *errors = NULL;
int error_count = 0;
cbm_pipeline_get_file_errors(p, &errors, &error_count);
int memo_errors = 0;
const char *expected_phase = strcmp(stage, "cross") == 0 ? "lsp_skipped" : "extract";
bool precise = true;
for (int i = 0; i < error_count; i++) {
if (errors[i].path && strcmp(errors[i].path, filename) == 0) {
memo_errors++;
precise = precise && errors[i].phase && strcmp(errors[i].phase, expected_phase) == 0 &&
errors[i].reason &&
strcmp(errors[i].reason,
depth ? (python ? CBM_PY_LSP_DEPTH_ERROR : CBM_C_LSP_DEPTH_ERROR)
: (python ? CBM_PY_LSP_MEMO_ERROR : CBM_C_LSP_MEMO_ERROR)) == 0;
}
}
cbm_pipeline_free(p);
int restore_rc = 0;
for (int i = 0; i < 3; i++) {
restore_rc |= saved[i] ? cbm_setenv(keys[i], saved[i], 1) : cbm_unsetenv(keys[i]);
free(saved[i]);
}
teardown_usages_repo();
ASSERT_EQ(set_rc, 0);
ASSERT_EQ(restore_rc, 0);
ASSERT_EQ(run_rc, 0);
ASSERT_EQ(memo_errors, 1);
ASSERT_TRUE(precise);
PASS();
}
TEST(pipeline_issue1527_cross_memo_failure_sequential) {
return pipeline_lsp_failure_case(false, "cross", false, false);
}
TEST(pipeline_issue1527_cross_memo_failure_parallel) {
return pipeline_lsp_failure_case(true, "cross", false, false);
}
TEST(pipeline_issue1527_raw_memo_failure_sequential) {
return pipeline_lsp_failure_case(false, "raw", false, false);
}
TEST(pipeline_issue1527_raw_memo_failure_parallel) {
return pipeline_lsp_failure_case(true, "raw", false, false);
}
TEST(pipeline_issue1527_python_raw_memo_failure_sequential) {
return pipeline_lsp_failure_case(false, "raw", true, false);
}
TEST(pipeline_issue1527_python_raw_memo_failure_parallel) {
return pipeline_lsp_failure_case(true, "raw", true, false);
}
TEST(pipeline_issue1527_python_cross_memo_failure_sequential) {
return pipeline_lsp_failure_case(false, "cross", true, false);
}
TEST(pipeline_issue1527_python_cross_memo_failure_parallel) {
return pipeline_lsp_failure_case(true, "cross", true, false);
}
TEST(pipeline_issue1527_c_raw_depth_failure_sequential) {
return pipeline_lsp_failure_case(false, "raw", false, true);
}
TEST(pipeline_issue1527_c_cross_depth_failure_sequential) {
return pipeline_lsp_failure_case(false, "cross", false, true);
}
TEST(pipeline_issue1527_c_raw_depth_failure_parallel) {
return pipeline_lsp_failure_case(true, "raw", false, true);
}
TEST(pipeline_issue1527_c_cross_depth_failure_parallel) {
return pipeline_lsp_failure_case(true, "cross", false, true);
}
TEST(pipeline_issue1527_python_raw_depth_failure_sequential) {
return pipeline_lsp_failure_case(false, "raw", true, true);
}
TEST(pipeline_issue1527_python_cross_depth_failure_sequential) {
return pipeline_lsp_failure_case(false, "cross", true, true);
}
TEST(pipeline_issue1527_python_raw_depth_failure_parallel) {
return pipeline_lsp_failure_case(true, "raw", true, true);
}
TEST(pipeline_issue1527_python_cross_depth_failure_parallel) {
return pipeline_lsp_failure_case(true, "cross", true, true);
}
TEST(pipeline_issue1527_cross_memo_failure_fallback) {
const char *src = "struct S { int m() { return 1; } }; void run() { S s; s.m(); }";
CBMLSPDef defs[] = {
{.qualified_name = "test.S", .short_name = "S", .label = "Class", .lang = CBM_LANG_CPP},
{.qualified_name = "test.S.m",
.short_name = "m",
.label = "Method",
.receiver_type = "test.S",
.return_types = "int",
.lang = CBM_LANG_CPP},
};
CBMFileResult result = {0};
cbm_arena_init(&result.arena);
cbm_c_lsp_test_memo_fail_after(0);
cbm_pxc_run_one(CBM_LANG_CPP, &result, src, (int)strlen(src), "test", defs, 2, NULL, NULL, 0);
cbm_c_lsp_test_memo_fail_after(-1);
bool reported = result.has_error && result.lsp_skipped && result.error_msg &&
strcmp(result.error_msg, CBM_C_LSP_MEMO_ERROR) == 0 &&
result.resolved_calls.count == 0;
cbm_arena_destroy(&result.arena);
ASSERT_TRUE(reported);
PASS();
}
/* #1277: typed instance fields of a Python class imported from another file.
* Counts CALLS edges caller -> callee (QN suffixes) whose properties name an
* LSP strategy. */
@@ -20205,6 +20370,23 @@ TEST(pipeline_calls_args_overlong_argument_stops_cleanly) {
}
SUITE(pipeline) {
RUN_TEST(pipeline_issue1527_python_raw_memo_failure_sequential);
RUN_TEST(pipeline_issue1527_python_raw_memo_failure_parallel);
RUN_TEST(pipeline_issue1527_python_cross_memo_failure_sequential);
RUN_TEST(pipeline_issue1527_python_cross_memo_failure_parallel);
RUN_TEST(pipeline_issue1527_c_raw_depth_failure_sequential);
RUN_TEST(pipeline_issue1527_c_cross_depth_failure_sequential);
RUN_TEST(pipeline_issue1527_c_raw_depth_failure_parallel);
RUN_TEST(pipeline_issue1527_c_cross_depth_failure_parallel);
RUN_TEST(pipeline_issue1527_python_raw_depth_failure_sequential);
RUN_TEST(pipeline_issue1527_python_cross_depth_failure_sequential);
RUN_TEST(pipeline_issue1527_python_raw_depth_failure_parallel);
RUN_TEST(pipeline_issue1527_python_cross_depth_failure_parallel);
RUN_TEST(pipeline_issue1527_cross_memo_failure_sequential);
RUN_TEST(pipeline_issue1527_cross_memo_failure_parallel);
RUN_TEST(pipeline_issue1527_raw_memo_failure_sequential);
RUN_TEST(pipeline_issue1527_raw_memo_failure_parallel);
RUN_TEST(pipeline_issue1527_cross_memo_failure_fallback);
RUN_TEST(pipeline_nested_fixture_files_are_written);
RUN_TEST(pipeline_fixture_file_parent_is_preserved);
RUN_TEST(pipeline_fixture_node_count_requires_exact_source_nodes);
+448 -15
View File
@@ -10,6 +10,7 @@
#include "test_framework.h"
#include "cbm.h"
#include "lsp/py_lsp.h"
#include "lsp/lsp_work.h"
#include "pipeline/lsp_resolve.h"
#include "pipeline/pass_lsp_cross.h"
@@ -2223,16 +2224,30 @@ TEST(pylsp_issue710_heterogeneous_receiver_chain) {
PASS();
}
TEST(pylsp_eval_steps_budget_degrades_gracefully) {
/* PY_EVAL_MAX_STEPS_PER_FILE guard: a file whose expressions demand
* more evaluator work than the per-file budget must still complete
* quickly and keep everything resolved BEFORE exhaustion — graceful
* partial degradation, not a hang, crash, or corrupted result. 90
* statements x 30-link chains re-evaluated across the bind + emit
* phases (the assignment bind flushes the memo generation in between)
* comfortably exceed the 10000-step budget. */
enum { BUDGET_STMTS = 90, BUDGET_LINKS = 30 };
size_t sz = 4096 + (size_t)BUDGET_STMTS * (32 + (size_t)BUDGET_LINKS * 8);
/* Resolved INVOCATION rows whose callee QN ends with `suffix` (e.g. ".compile"). */
static int count_resolved_suffix(const CBMFileResult *r, const char *suffix) {
size_t sl = strlen(suffix);
int n = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
size_t ql = rc->callee_qn ? strlen(rc->callee_qn) : 0;
if (rc->kind == CBM_RESOLVED_INVOCATION && ql >= sl &&
strcmp(rc->callee_qn + ql - sl, suffix) == 0) {
n++;
}
}
return n;
}
TEST(pylsp_issue1527_chain_statements_resolve_completely) {
/* #1527 replaces the old budget-degradation guard: with no per-file
* evaluation budget, the #710 chain shape -- 90 statements x 30-link
* chains, re-evaluated across the bind + emit phases (the assignment bind
* flushes the memo generation in between) -- must still finish quickly
* AND resolve EVERY link and EVERY final call (each is a distinct call
* site), not merely the first statements. */
enum { CHAIN_STMTS = 90, CHAIN_LINKS = 30 };
size_t sz = 4096 + (size_t)CHAIN_STMTS * (32 + (size_t)CHAIN_LINKS * 8);
char *src = malloc(sz);
ASSERT_NOT_NULL(src);
char *p = src;
@@ -2247,11 +2262,11 @@ TEST(pylsp_eval_steps_budget_degrades_gracefully) {
"def run():\n");
p += n;
left -= (size_t)n;
for (int s = 0; s < BUDGET_STMTS; s++) {
for (int s = 0; s < CHAIN_STMTS; s++) {
n = snprintf(p, left, " v%d = G()", s);
p += n;
left -= (size_t)n;
for (int l = 0; l < BUDGET_LINKS; l++) {
for (int l = 0; l < CHAIN_LINKS; l++) {
n = snprintf(p, left, ".add(%d)", l);
p += n;
left -= (size_t)n;
@@ -2264,15 +2279,432 @@ TEST(pylsp_eval_steps_budget_degrades_gracefully) {
CBMFileResult *r = extract_py(src);
free(src);
ASSERT_NOT_NULL(r);
/* The first statements run with budget to spare — they must resolve. */
ASSERT_GTE(require_resolved(r, "run", "G.compile"), 0);
ASSERT_EQ(count_resolved_suffix(r, ".G.compile"), CHAIN_STMTS);
ASSERT_EQ(count_resolved_suffix(r, ".G.add"), CHAIN_STMTS * CHAIN_LINKS);
cbm_free_result(r);
PASS();
}
TEST(pylsp_issue1527_large_module_resolves_every_call) {
/* #1527 reproduction: a module of 8000 classes plus one function calling
* `ClsN().chain().chain().method()` for each. Before the fix the per-file
* step cap stopped resolution after ~3333 statements (CALLS 14,666 of
* 24,000 in the graph) while the file still reported status `indexed`.
* Every one of the 8000 x 3 method call sites must resolve. */
enum { N_CLASSES = 8000 };
size_t sz = (size_t)N_CLASSES * 192 + 1024;
char *src = malloc(sz);
ASSERT_NOT_NULL(src);
size_t pos = (size_t)snprintf(src, sz, "from typing import Self\n");
for (int i = 0; i < N_CLASSES; i++) {
pos += (size_t)snprintf(src + pos, sz - pos,
"class Cls%d:\n"
" def method(self) -> int:\n"
" return %d\n"
" def chain(self) -> Self:\n"
" return self\n",
i, i);
}
pos += (size_t)snprintf(src + pos, sz - pos, "def use():\n");
for (int i = 0; i < N_CLASSES; i++) {
pos += (size_t)snprintf(src + pos, sz - pos, " Cls%d().chain().chain().method()\n", i);
}
ASSERT(pos < sz);
CBMFileResult *r =
cbm_extract_file(src, (int)pos, CBM_LANG_PYTHON, "test", "scale.py", 0, NULL, NULL);
free(src);
ASSERT_NOT_NULL(r);
ASSERT_EQ(count_resolved_suffix(r, ".method"), N_CLASSES);
ASSERT_EQ(count_resolved_suffix(r, ".chain"), 2 * N_CLASSES);
/* The last class is resolved exactly, not just some class. */
ASSERT_GTE(require_resolved(r, "use", "Cls7999.method"), 0);
cbm_free_result(r);
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────── */
static char *pylsp_work_fixture(int n_classes, int *out_len) {
/* Per class: ~140 chars (5-line def). Per call: ~50 chars. Overhead
* for the class number digits scales with log10(n) but the constant
* 256 covers up to 9-digit indices comfortably. */
int approx = n_classes * 256 + 1024;
char *buf = (char *)malloc((size_t)approx);
if (!buf)
return NULL;
int pos = 0;
pos += snprintf(buf + pos, (size_t)(approx - pos), "from typing import Self\n");
for (int i = 0; i < n_classes; i++) {
int n = snprintf(buf + pos, (size_t)(approx - pos),
"class Cls%d:\n"
" def method(self) -> int:\n"
" return %d\n"
" def chain(self) -> Self:\n"
" return self\n",
i, i);
if (n < 0 || pos + n >= approx)
break;
pos += n;
}
int n = snprintf(buf + pos, (size_t)(approx - pos), "def use():\n");
pos += n;
for (int i = 0; i < n_classes; i++) {
int m = snprintf(buf + pos, (size_t)(approx - pos),
" Cls%d().chain().chain().method()\n", i);
if (m < 0 || pos + m >= approx)
break;
pos += m;
}
*out_len = pos;
return buf;
}
/* Deterministic complexity gate (#1527, O9: no clock). The resolver
* primitives count every candidate they examine -- scope-frame probes,
* registry lookup probes, enclosing-definition candidates -- plus every real
* (non-memoized) expression evaluation, into a seam-build work counter. 4x
* the input must cost about 4x the work: a linear-scan lookup or a lost memo
* lands at ~16x (quadratic) or worse. Also asserts every call site resolved,
* so the work is not "linear" because resolution stopped early. */
static uint64_t work_for(int n_classes, int *out_resolved) {
int slen = 0;
char *src = pylsp_work_fixture(n_classes, &slen);
if (!src)
return 0;
(void)cbm_lsp_work_take();
CBMFileResult *r =
cbm_extract_file(src, slen, CBM_LANG_PYTHON, "test", "scale.py", 0, NULL, NULL);
uint64_t work = cbm_lsp_work_take();
*out_resolved = r ? r->resolved_calls.count : -1;
if (r)
cbm_free_result(r);
free(src);
return work;
}
TEST(pylsp_scale_work_is_linear) {
int r1 = 0, r4 = 0;
uint64_t w1 = work_for(1000, &r1);
uint64_t w4 = work_for(4000, &r4);
double ratio = w1 ? (double)w4 / (double)w1 : 0.0;
printf(" work: 1000=%llu (resolved=%d) 4000=%llu (resolved=%d) ratio=%.2fx "
"(linear ~4x, quadratic ~16x)\n",
(unsigned long long)w1, r1, (unsigned long long)w4, r4, ratio);
/* Each class line: ClsN() + chain() + chain() + method() = 4 resolved sites. */
ASSERT_EQ(r1, 4 * 1000);
ASSERT_EQ(r4, 4 * 4000);
ASSERT_GT(w1, 0);
ASSERT(ratio < 5.0);
PASS();
}
extern const TSLanguage *tree_sitter_python(void);
TEST(pylsp_issue1527_memo_allocation_failure_is_reported) {
int len = 0;
char *src = pylsp_work_fixture(200, &len);
ASSERT_NOT_NULL(src);
bool failures_reported = true;
for (int fail_after = 0; fail_after <= 1; fail_after++) {
cbm_py_lsp_test_memo_fail_after(fail_after);
CBMFileResult *r = extract_py(src);
cbm_py_lsp_test_memo_fail_after(-1);
failures_reported = failures_reported && r && r->has_error && r->lsp_skipped &&
r->error_msg && strcmp(r->error_msg, CBM_PY_LSP_MEMO_ERROR) == 0 &&
r->resolved_calls.count == 0;
if (r) {
cbm_result_compact(r);
failures_reported = failures_reported && r->error_msg &&
strcmp(r->error_msg, CBM_PY_LSP_MEMO_ERROR) == 0;
cbm_free_result(r);
}
}
CBMFileResult *fresh = extract_py(src);
bool recovered = fresh && !fresh->has_error && fresh->resolved_calls.count > 0;
if (fresh)
cbm_free_result(fresh);
free(src);
ASSERT_TRUE(failures_reported);
ASSERT_TRUE(recovered);
PASS();
}
TEST(pylsp_issue1527_memo_and_depth_failures_stop_work) {
const char *src = "x\n";
TSParser *parser = ts_parser_new();
ASSERT_NOT_NULL(parser);
ts_parser_set_language(parser, tree_sitter_python());
TSTree *tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)strlen(src));
if (!tree) {
ts_parser_delete(parser);
FAIL("failed to parse memo fixture");
}
TSNode node = ts_node_named_child(ts_node_named_child(ts_tree_root_node(tree), 0), 0);
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry reg;
cbm_registry_init(&reg, &arena);
CBMResolvedCallArray out = {0};
PyLSPContext ctx;
py_lsp_init(&ctx, &arena, src, (int)strlen(src), &reg, "test", &out);
const CBMType *known = cbm_type_named(&arena, "test.Known");
cbm_scope_bind(ctx.current_scope, "x", known);
ctx.eval_depth = 256;
bool depth_unknown = cbm_type_is_unknown(cbm_py_lsp_test_eval(&ctx, node)) &&
ctx.eval_failure == CBM_LSP_DEPTH_EXCEEDED;
ctx.eval_depth = 0;
(void)cbm_lsp_work_take();
bool depth_stopped = cbm_type_is_unknown(cbm_py_lsp_test_eval(&ctx, node));
depth_stopped = depth_stopped && cbm_lsp_work_take() == 0;
/* A fresh walk can resolve this shallow expression; the failed one cannot. */
py_lsp_init(&ctx, &arena, src, (int)strlen(src), &reg, "test", &out);
cbm_scope_bind(ctx.current_scope, "x", known);
bool depth_recovered = cbm_py_lsp_test_eval(&ctx, node) == known && !ctx.eval_failure;
bool stopped = true;
for (int capacity_failure = 0; capacity_failure < 2; capacity_failure++) {
py_lsp_init(&ctx, &arena, src, (int)strlen(src), &reg, "test", &out);
cbm_scope_bind(ctx.current_scope, "x", known);
ctx.test_memo_allocations_left = 0;
/* Pin the maximum-capacity refusal without allocating a huge table.
* A NULL table bypasses lookup; insertion must report failed growth. */
if (capacity_failure)
ctx.type_cache_cap = 1 << 26;
stopped =
stopped && cbm_type_is_unknown(cbm_py_lsp_test_eval(&ctx, node)) && ctx.eval_failure;
(void)cbm_lsp_work_take();
for (int i = 0; i < 20; i++)
stopped = cbm_type_is_unknown(cbm_py_lsp_test_eval(&ctx, node)) && stopped;
stopped = stopped && cbm_lsp_work_take() == 0;
}
cbm_arena_destroy(&arena);
ts_tree_delete(tree);
ts_parser_delete(parser);
ASSERT_TRUE(depth_unknown);
ASSERT_TRUE(depth_stopped);
ASSERT_TRUE(depth_recovered);
ASSERT_TRUE(stopped);
PASS();
}
TEST(pylsp_issue1527_root_class_name_failure_binds_no_external_class) {
const char *src = "class Local:\n pass\n";
TSParser *parser = ts_parser_new();
ASSERT_NOT_NULL(parser);
ts_parser_set_language(parser, tree_sitter_python());
TSTree *tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)strlen(src));
if (!tree) {
ts_parser_delete(parser);
FAIL("failed to parse root-name fixture");
}
bool closed = true, control = false;
for (int failure = 0; failure <= 2; failure++) {
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry reg;
cbm_registry_init(&reg, &arena);
cbm_registry_add_type(
&reg, (CBMRegisteredType){.qualified_name = "test.Local", .short_name = "Local"});
cbm_registry_add_type(
&reg, (CBMRegisteredType){.qualified_name = "test.External", .short_name = "External"});
CBMResolvedCallArray out = {0};
PyLSPContext ctx;
py_lsp_init(&ctx, &arena, src, (int)strlen(src), &reg, "test", &out);
ctx.test_root_name_failure = failure;
cbm_py_lsp_test_bind_external_classes(&ctx, ts_tree_root_node(tree));
bool local_absent = !cbm_scope_lookup_local(ctx.current_scope, "Local");
bool external_present = cbm_scope_lookup_local(ctx.current_scope, "External") != NULL;
if (failure)
closed =
closed && ctx.callable_value_proof_disabled && local_absent && !external_present;
else
control = !ctx.callable_value_proof_disabled && local_absent && external_present;
cbm_arena_destroy(&arena);
}
ts_tree_delete(tree);
ts_parser_delete(parser);
ASSERT_TRUE(control);
ASSERT_TRUE(closed);
PASS();
}
TEST(pylsp_issue1527_cross_memo_failure_preserves_completed_rows) {
char src[4096];
size_t pos = (size_t)snprintf(src, sizeof(src), "def run(s: S):\n s + s\n");
for (int i = 0; i < 200; i++)
pos += (size_t)snprintf(src + pos, sizeof(src) - pos, " s.m()\n");
CBMLSPDef defs[] = {
{.qualified_name = "test.S", .short_name = "S", .label = "Class", .lang = CBM_LANG_PYTHON},
{.qualified_name = "test.S.m",
.short_name = "m",
.label = "Method",
.receiver_type = "test.S",
.return_types = "int",
.lang = CBM_LANG_PYTHON},
{.qualified_name = "test.S.__add__",
.short_name = "__add__",
.label = "Method",
.receiver_type = "test.S",
.return_types = "S",
.lang = CBM_LANG_PYTHON},
};
bool correct = true;
for (int mode = 0; mode < 3; mode++) {
CBMArena arena, shared;
cbm_arena_init(&arena);
cbm_arena_init(&shared);
CBMResolvedCall prior = {.caller_qn = "test.run",
.callee_qn = "test.S.__add__",
.kind = CBM_RESOLVED_INVOCATION,
.confidence = 0.1f,
.strategy = "prior.strategy",
.reason = "prior.reason",
.site_start_byte = (uint32_t)(strstr(src, "s + s") - src),
.site_end_byte = (uint32_t)(strstr(src, "s + s") - src) + 5};
CBMCall prior_synthetic = {.callee_name = "completed.synthetic"};
CBMResolvedCallArray out = {.items = &prior, .count = 1, .cap = 1};
CBMCallArray synthetic = {.items = &prior_synthetic, .count = 1, .cap = 1};
CBMTypeRegistry *base = mode == 1 ? cbm_py_build_cross_registry(&shared, defs, 3) : NULL;
CBMTypeRegistry overlay;
cbm_registry_init(&overlay, &arena);
overlay.fallback = base;
CBMBatchPyLSPFile file = {.source = src,
.source_len = (int)strlen(src),
.module_qn = "test",
.defs = defs,
.def_count = 3};
cbm_py_lsp_test_memo_fail_after(1);
bool complete =
mode == 0 ? cbm_run_py_lsp_cross(&arena, src, (int)strlen(src), "test", defs, 3, NULL,
NULL, 0, NULL, &out, &synthetic)
: mode == 1 ? cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test",
&overlay, NULL, NULL, 0, NULL, &out,
&synthetic)
: cbm_batch_py_lsp_cross(&arena, &file, 1, &out);
cbm_py_lsp_test_memo_fail_after(-1);
correct = correct && !complete && out.count == (mode == 2 ? 0 : 1) &&
synthetic.count == 1 && prior.confidence == 0.1f &&
strcmp(prior.strategy, "prior.strategy") == 0 &&
strcmp(prior.reason, "prior.reason") == 0 &&
strcmp(prior_synthetic.callee_name, "completed.synthetic") == 0;
if (mode != 2)
correct = correct && out.items[0].confidence == 0.1f &&
strcmp(out.items[0].strategy, "prior.strategy") == 0 &&
strcmp(out.items[0].reason, "prior.reason") == 0;
if (mode == 2)
out = (CBMResolvedCallArray){0};
complete = mode == 0 ? cbm_run_py_lsp_cross(&arena, src, (int)strlen(src), "test", defs, 3,
NULL, NULL, 0, NULL, &out, &synthetic)
: mode == 1 ? cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src),
"test", &overlay, NULL, NULL, 0,
NULL, &out, &synthetic)
: cbm_batch_py_lsp_cross(&arena, &file, 1, &out);
correct = correct && complete && out.count > 200 &&
(mode == 2 || (synthetic.count > 1 && out.items[0].confidence > 0.1f));
cbm_arena_destroy(&arena);
cbm_arena_destroy(&shared);
}
ASSERT_TRUE(correct);
PASS();
}
TEST(pylsp_issue1527_cross_memo_failure_fallback_reports_error) {
const char *src = "def run(s: S):\n s.m()\n";
CBMLSPDef defs[] = {
{.qualified_name = "test.S", .short_name = "S", .label = "Class", .lang = CBM_LANG_PYTHON},
{.qualified_name = "test.S.m",
.short_name = "m",
.label = "Method",
.receiver_type = "test.S",
.return_types = "int",
.lang = CBM_LANG_PYTHON},
};
CBMFileResult result = {0};
cbm_arena_init(&result.arena);
cbm_py_lsp_test_memo_fail_after(0);
cbm_pxc_run_one(CBM_LANG_PYTHON, &result, src, (int)strlen(src), "test", defs, 2, NULL, NULL,
0);
cbm_py_lsp_test_memo_fail_after(-1);
bool reported = result.has_error && result.lsp_skipped && result.error_msg &&
strcmp(result.error_msg, CBM_PY_LSP_MEMO_ERROR) == 0 &&
result.resolved_calls.count == 0;
cbm_arena_destroy(&result.arena);
ASSERT_TRUE(reported);
PASS();
}
TEST(pylsp_issue1527_depth_failure_reports_all_entry_points) {
const char *src = "class S:\n def m(self):\n return 1\ndef run(s: S):\n s.m()\n";
CBMLSPDef defs[] = {
{.qualified_name = "test.S", .short_name = "S", .label = "Class", .lang = CBM_LANG_PYTHON},
{.qualified_name = "test.S.m",
.short_name = "m",
.label = "Method",
.receiver_type = "test.S",
.return_types = "int",
.lang = CBM_LANG_PYTHON},
};
cbm_py_lsp_test_depth_fail(true);
CBMFileResult *raw = extract_py(src);
bool correct = raw && raw->has_error && raw->lsp_skipped && raw->error_msg &&
strcmp(raw->error_msg, CBM_PY_LSP_DEPTH_ERROR) == 0 &&
raw->resolved_calls.count == 0;
if (raw)
cbm_free_result(raw);
for (int mode = 0; mode < 3; mode++) {
CBMArena arena, shared;
cbm_arena_init(&arena);
cbm_arena_init(&shared);
CBMTypeRegistry *base = mode == 1 ? cbm_py_build_cross_registry(&shared, defs, 2) : NULL;
CBMTypeRegistry overlay;
cbm_registry_init(&overlay, &arena);
overlay.fallback = base;
CBMResolvedCallArray out = {0};
CBMBatchPyLSPFile file = {.source = src,
.source_len = (int)strlen(src),
.module_qn = "test",
.defs = defs,
.def_count = 2};
if (mode == 2) {
correct = !cbm_batch_py_lsp_cross(&arena, &file, 1, &out) && correct;
} else {
CBMLSPStatus status =
mode == 0 ? cbm_run_py_lsp_cross_status(&arena, src, (int)strlen(src), "test", defs,
2, NULL, NULL, 0, NULL, &out, NULL)
: cbm_run_py_lsp_cross_with_registry_status(&arena, src, (int)strlen(src),
"test", &overlay, NULL, NULL,
0, NULL, &out, NULL);
correct = correct && status == CBM_LSP_DEPTH_EXCEEDED;
}
correct = correct && out.count == 0;
cbm_arena_destroy(&arena);
cbm_arena_destroy(&shared);
}
CBMFileResult fallback = {0};
cbm_arena_init(&fallback.arena);
cbm_pxc_run_one(CBM_LANG_PYTHON, &fallback, src, (int)strlen(src), "test", defs, 2, NULL, NULL,
0);
correct = correct && fallback.has_error && fallback.lsp_skipped && fallback.error_msg &&
strcmp(fallback.error_msg, CBM_PY_LSP_DEPTH_ERROR) == 0 &&
fallback.resolved_calls.count == 0;
cbm_arena_destroy(&fallback.arena);
cbm_py_lsp_test_depth_fail(false);
CBMFileResult *fresh = extract_py(src);
bool recovered = fresh && !fresh->has_error && fresh->resolved_calls.count > 0;
if (fresh)
cbm_free_result(fresh);
ASSERT_TRUE(correct);
ASSERT_TRUE(recovered);
PASS();
}
SUITE(py_lsp) {
RUN_TEST(pylsp_issue1527_depth_failure_reports_all_entry_points);
RUN_TEST(pylsp_issue1527_cross_memo_failure_fallback_reports_error);
RUN_TEST(pylsp_issue1527_cross_memo_failure_preserves_completed_rows);
RUN_TEST(pylsp_issue1527_root_class_name_failure_binds_no_external_class);
RUN_TEST(pylsp_issue1527_memo_and_depth_failures_stop_work);
RUN_TEST(pylsp_issue1527_memo_allocation_failure_is_reported);
RUN_TEST(pylsp_scale_work_is_linear);
/* Phase 2 — smoke */
RUN_TEST(pylsp_smoke_empty);
RUN_TEST(pylsp_smoke_one_function);
@@ -2366,5 +2798,6 @@ SUITE(py_lsp) {
/* Evaluator guards — #710/#720 (distilled PRs #732 + #758) */
RUN_TEST(pylsp_issue710_deep_call_chain_resolves);
RUN_TEST(pylsp_issue710_heterogeneous_receiver_chain);
RUN_TEST(pylsp_eval_steps_budget_degrades_gracefully);
RUN_TEST(pylsp_issue1527_chain_statements_resolve_completely);
RUN_TEST(pylsp_issue1527_large_module_resolves_every_call);
}
+6 -22
View File
@@ -1,7 +1,7 @@
/*
* test_py_lsp_scale.c — measure scaling behavior at 100 / 500 / 2000
* classes-and-calls. Asserts that doubling the input doesn't more than
* 4x the runtime (catches accidental O(n^2) in the resolver).
* classes-and-calls. Timing is diagnostic; deterministic work is gated in
* the normal py_lsp suite.
*/
#include "test_framework.h"
#include "cbm.h"
@@ -104,26 +104,10 @@ TEST(pylsp_scale_linear_growth) {
ASSERT(r_pct_100 > 0.5);
ASSERT(r_pct_2000 > 0.5);
/* Quadratic-growth detector. 20x input: linear ~20-30x time, clear
* quadratic ~400x. The bound sits at 200x — mid-way in log space — so a
* real quadratic regression still fails by 2x while the CURRENT, KNOWN
* superlinear resolve curve does not produce false release blocks.
*
* The old bound of 100x was calibrated as "linear plus generous overhead",
* but the resolve path has never been linear here: measured 2026-08-11
* (sanitized builds, deterministic across runs) — quiet arm64 host
* 57-68x, macos-15-intel CI runner 101.1x, per-function cost growing
* 0.5ms -> 1.7ms from 100 to 2000 functions. That ~O(n^1.4-1.5) curve is
* the audited short-name-lookup/negative-memo gap, tracked as #1527; this
* detector was flagging host CONSTANTS, not a complexity change. When
* #1527 lands, tighten this back down (~40x holds linear honestly). */
if (t100 > 0.5) { // skip when t100 too small to compare reliably
double ratio = t2000 / t100;
printf(" scale ratio 2000/100: %.1fx (linear ~20x, known-superlinear ~60-100x, "
"quadratic ~400x)\n",
ratio);
ASSERT(ratio < 200.0); // flags clear quadratic; #1527 tracks the curve itself
}
/* Timing is diagnostic only. The normal py_lsp suite gates deterministic
* resolver work and complete resolution independently of host scheduling. */
if (t100 > 0.0)
printf(" diagnostic scale ratio 2000/100: %.1fx\n", t2000 / t100);
PASS();
}