mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
5d4b50fe1c
commit
c3c97a99d7
21 files changed
+2493
-282
No files matched your search
+185
-18
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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, ®, 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(®, rt);
|
||||
c_add_type_indexed(®, &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 = ®.types[ri];
|
||||
break;
|
||||
}
|
||||
}
|
||||
int parent_idx = c_type_first_find(&first_ix, ®, d->parent_class);
|
||||
CBMRegisteredType *parent_rt = parent_idx >= 0 ? ®.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(®, d->parent_class)) {
|
||||
if (c_type_first_find(&first_ix, ®, 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(®, auto_type);
|
||||
c_add_type_indexed(®, &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(®, &idx_arena);
|
||||
|
||||
// Initialize context and run
|
||||
CLSPContext ctx;
|
||||
c_lsp_init(&ctx, arena, source, source_len, ®, 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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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(®);
|
||||
|
||||
/* 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(®, &idx_arena);
|
||||
|
||||
PyLSPContext ctx;
|
||||
py_lsp_init(&ctx, arena, source, source_len, ®, 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 ─── */
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = ®->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 = ®->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 = ®->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 = ®->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 ®->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 ®->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 ®->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 ®->funcs[i];
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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++;
|
||||
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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(®, &a);
|
||||
CBMResolvedCallArray out = {0};
|
||||
CLSPContext ctx;
|
||||
c_lsp_init(&ctx, &a, src, (int)strlen(src), ®, "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), ®, "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(®, &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,
|
||||
®, 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, ®, 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(®, &a);
|
||||
CBMRegisteredType t = {.qualified_name = "test.S", .short_name = "S"};
|
||||
cbm_registry_add_type(®, t);
|
||||
cbm_registry_add_type(®, t);
|
||||
CBMRegisteredFunc f = {
|
||||
.qualified_name = "test.S.m", .short_name = "m", .receiver_type = "test.S"};
|
||||
cbm_registry_add_func(®, f);
|
||||
cbm_registry_add_func(®, f);
|
||||
bool linear_first = cbm_registry_lookup_type(®, "test.S") == ®.types[0] &&
|
||||
cbm_registry_lookup_func(®, "test.S.m") == ®.funcs[0] &&
|
||||
cbm_registry_lookup_method(®, "test.S", "m") == ®.funcs[0];
|
||||
reg.index_first_registered = true;
|
||||
cbm_registry_finalize(®);
|
||||
bool indexed_first = cbm_registry_lookup_type(®, "test.S") == ®.types[0] &&
|
||||
cbm_registry_lookup_func(®, "test.S.m") == ®.funcs[0] &&
|
||||
cbm_registry_lookup_method(®, "test.S", "m") == ®.funcs[0];
|
||||
reg.index_first_registered = false;
|
||||
cbm_registry_finalize(®);
|
||||
bool default_last = cbm_registry_lookup_type(®, "test.S") == ®.types[1] &&
|
||||
cbm_registry_lookup_func(®, "test.S.m") == ®.funcs[1] &&
|
||||
cbm_registry_lookup_method(®, "test.S", "m") == ®.funcs[1];
|
||||
f.qualified_name = "test.S.tail";
|
||||
f.short_name = "tail";
|
||||
cbm_registry_add_func(®, f);
|
||||
bool tail = cbm_registry_lookup_func(®, "test.S.tail") == ®.funcs[2] &&
|
||||
cbm_registry_lookup_method(®, "test.S", "tail") == ®.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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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(®, &arena);
|
||||
CBMResolvedCallArray out = {0};
|
||||
PyLSPContext ctx;
|
||||
py_lsp_init(&ctx, &arena, src, (int)strlen(src), ®, "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), ®, "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), ®, "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(®, &arena);
|
||||
cbm_registry_add_type(
|
||||
®, (CBMRegisteredType){.qualified_name = "test.Local", .short_name = "Local"});
|
||||
cbm_registry_add_type(
|
||||
®, (CBMRegisteredType){.qualified_name = "test.External", .short_name = "External"});
|
||||
CBMResolvedCallArray out = {0};
|
||||
PyLSPContext ctx;
|
||||
py_lsp_init(&ctx, &arena, src, (int)strlen(src), ®, "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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user