Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 132 additions & 26 deletions internal/cbm/lsp/ts_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ void cbm_ts_full_registry_builds_reset(void) {
* constructs it does not model. -1 = unlimited (no entry point armed it). */
static _Thread_local long g_ts_type_budget = -1;
static _Thread_local bool g_ts_type_budget_warned;
#ifdef CBM_ENABLE_TEST_SEAMS
static _Thread_local int g_ts_max_member_lookup_steps;
static _Thread_local int g_ts_max_method_lookup_steps;
#endif

static void ts_type_budget_reset(size_t source_len) {
const char *e = getenv("CBM_TS_TYPE_BUDGET");
Expand Down Expand Up @@ -169,6 +173,12 @@ long cbm_ts_lsp_test_budget_remaining(void) {
bool cbm_ts_lsp_test_budget_warned(void) {
return g_ts_type_budget_warned;
}
int cbm_ts_lsp_test_max_member_lookup_steps(void) {
return g_ts_max_member_lookup_steps;
}
int cbm_ts_lsp_test_max_method_lookup_steps(void) {
return g_ts_max_method_lookup_steps;
}
#endif

#define TS_LSP_MAX_EVAL_DEPTH 64
Expand All @@ -189,6 +199,10 @@ static const CBMType *type_of_identifier(TSLSPContext *ctx, const char *name);
static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name);
static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *recv,
const char *method_name);
typedef struct TSLookupState TSLookupState;
static const CBMRegisteredFunc *lookup_method_at(TSLSPContext *ctx, const CBMType *recv,
const char *method_name, int depth,
TSLookupState *state);
static char *node_text(TSLSPContext *ctx, TSNode node);

// Collect a node's children into an arena array via a single O(n) cursor pass.
Expand Down Expand Up @@ -1559,28 +1573,77 @@ static const char *builtin_wrapper_class(const char *builtin_name) {
}

// Look up a property `name` on a receiver type. Returns the property type or UNKNOWN.
static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType *recv,
const char *name);

#define TS_LSP_MAX_MEMBER_DEPTH 64
#define TS_LSP_INLINE_VISITED_TYPES 16

typedef struct {
const char *qn;
int depth;
} TSVisitedType;

struct TSLookupState {
TSVisitedType inline_visited[TS_LSP_INLINE_VISITED_TYPES];
TSVisitedType *visited;
int visited_count;
int visited_capacity;
#ifdef CBM_ENABLE_TEST_SEAMS
int steps;
#endif
};

/* A named type needs another visit only when reached along a shallower path.
* Grow the visited table rather than silently dropping entries at a fixed cap:
* the depth bound then limits each named type to at most 64 admitted visits. */
static bool ts_lookup_visit_named(CBMArena *arena, TSLookupState *state, const char *qn,
int depth) {
for (int i = 0; i < state->visited_count; i++) {
if (strcmp(state->visited[i].qn, qn) == 0) {
if (state->visited[i].depth <= depth)
return false;
state->visited[i].depth = depth;
return true;
}
}
if (!state->visited) {
state->visited = state->inline_visited;
state->visited_capacity = TS_LSP_INLINE_VISITED_TYPES;
} else if (state->visited_count == state->visited_capacity) {
int capacity = state->visited_capacity * 2;
TSVisitedType *larger = (TSVisitedType *)cbm_arena_alloc(
arena, (size_t)capacity * sizeof(*larger));
if (!larger) {
fprintf(stderr, "[tslsp] lookup visited allocation failed\n");
return false;
}
memcpy(larger, state->visited, (size_t)state->visited_count * sizeof(*larger));
state->visited = larger;
state->visited_capacity = capacity;
}
state->visited[state->visited_count++] = (TSVisitedType){.qn = qn, .depth = depth};
return true;
}

/* Depth-guarded entry: member lookup recurses through wrapper classes, union
* members and registered-type expansion; cyclic type graphs in real-world TS
* (microsoft/TypeScript reallyLargeFile.ts) recursed without bound — SIGBUS
* stack overflow under endless lookup_member_type frames. Past the cap the
* member resolves as unknown — graceful degradation, not a crash. */
static const CBMType *lookup_member_type_at(TSLSPContext *ctx, const CBMType *recv,
const char *name, int depth, TSLookupState *state);

/* Depth and shallower-only named-type visits bound work on cyclic inheritance. */
static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name) {
if (!ctx || ctx->member_depth >= TS_LSP_MAX_MEMBER_DEPTH)
return cbm_type_unknown();
ctx->member_depth++;
const CBMType *r = lookup_member_type_inner(ctx, recv, name);
ctx->member_depth--;
return r;
TSLookupState state = {0};
const CBMType *result = lookup_member_type_at(ctx, recv, name, 0, &state);
#ifdef CBM_ENABLE_TEST_SEAMS
if (state.steps > g_ts_max_member_lookup_steps)
g_ts_max_member_lookup_steps = state.steps;
#endif
return result;
}

static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType *recv,
const char *name) {
if (!ctx || !recv || !name)
static const CBMType *lookup_member_type_at(TSLSPContext *ctx, const CBMType *recv,
const char *name, int depth, TSLookupState *state) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (state)
state->steps++;
#endif
if (!ctx || !recv || !name || !state || depth >= TS_LSP_MAX_MEMBER_DEPTH)
return cbm_type_unknown();
const CBMType *base = simplify_type(ctx, recv);
if (!base)
Expand All @@ -1594,7 +1657,7 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
const char *wrap = builtin_wrapper_class(base->data.builtin.name);
if (wrap) {
const CBMType *wrapped = cbm_type_named(ctx->arena, wrap);
return lookup_member_type(ctx, wrapped, name);
return lookup_member_type_at(ctx, wrapped, name, depth + 1, state);
}
return cbm_type_unknown();
}
Expand Down Expand Up @@ -1649,7 +1712,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
// first hit instead of building a union of results.)
if (base->data.union_type.members) {
for (int i = 0; i < base->data.union_type.count; i++) {
const CBMType *m = lookup_member_type(ctx, base->data.union_type.members[i], name);
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *m = lookup_member_type_at(
ctx, base->data.union_type.members[i], name, depth + 1, state);
if (!cbm_type_is_unknown(m))
return m;
}
Expand All @@ -1660,7 +1726,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
if (base->kind == CBM_TYPE_INTERSECTION) {
if (base->data.union_type.members) {
for (int i = 0; i < base->data.union_type.count; i++) {
const CBMType *m = lookup_member_type(ctx, base->data.union_type.members[i], name);
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *m = lookup_member_type_at(
ctx, base->data.union_type.members[i], name, depth + 1, state);
if (!cbm_type_is_unknown(m))
return m;
}
Expand All @@ -1672,6 +1741,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
return cbm_type_unknown();

const char *recv_qn = base->data.named.qualified_name;
if (!recv_qn)
return cbm_type_unknown();
if (!ts_lookup_visit_named(ctx->arena, state, recv_qn, depth))
return cbm_type_unknown();
const CBMRegisteredType *rt = cbm_registry_resolve_alias(ctx->registry, recv_qn);
if (!rt) {
rt = cbm_registry_lookup_type(ctx->registry, recv_qn);
Expand Down Expand Up @@ -1701,8 +1774,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
// Walk extends/implements.
if (rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *parent = cbm_type_named(ctx->arena, rt->embedded_types[i]);
const CBMType *m = lookup_member_type(ctx, parent, name);
const CBMType *m = lookup_member_type_at(ctx, parent, name, depth + 1, state);
if (!cbm_type_is_unknown(m))
return m;
}
Expand Down Expand Up @@ -1788,7 +1863,24 @@ static const CBMType *eval_indexed_access(TSLSPContext *ctx, const CBMType *obj,
// Look up a method on a receiver type — returns the registered func.
static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *recv,
const char *method_name) {
if (!ctx || !recv || !method_name)
TSLookupState state = {0};
const CBMRegisteredFunc *result = lookup_method_at(ctx, recv, method_name, 0, &state);
#ifdef CBM_ENABLE_TEST_SEAMS
if (state.steps > g_ts_max_method_lookup_steps)
g_ts_max_method_lookup_steps = state.steps;
#endif
return result;
}

/* Depth and shallower-only named-type visits bound cyclic inheritance work. */
static const CBMRegisteredFunc *lookup_method_at(TSLSPContext *ctx, const CBMType *recv,
const char *method_name, int depth,
TSLookupState *state) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (state)
state->steps++;
#endif
if (!ctx || !recv || !method_name || !state || depth >= TS_LSP_MAX_MEMBER_DEPTH)
return NULL;
const CBMType *base = simplify_type(ctx, recv);
if (!base)
Expand All @@ -1802,7 +1894,7 @@ static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *
const char *wrap = builtin_wrapper_class(base->data.builtin.name);
if (wrap) {
const CBMType *wrapped = cbm_type_named(ctx->arena, wrap);
return lookup_method(ctx, wrapped, method_name);
return lookup_method_at(ctx, wrapped, method_name, depth + 1, state);
}
return NULL;
}
Expand Down Expand Up @@ -1831,8 +1923,10 @@ static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *
if (base->kind == CBM_TYPE_UNION || base->kind == CBM_TYPE_INTERSECTION) {
if (base->data.union_type.members) {
for (int i = 0; i < base->data.union_type.count; i++) {
const CBMRegisteredFunc *f =
lookup_method(ctx, base->data.union_type.members[i], method_name);
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMRegisteredFunc *f = lookup_method_at(
ctx, base->data.union_type.members[i], method_name, depth + 1, state);
if (f)
return f;
}
Expand All @@ -1843,6 +1937,11 @@ static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *
if (base->kind != CBM_TYPE_NAMED)
return NULL;
const char *recv_qn = base->data.named.qualified_name;
if (!recv_qn)
return NULL;

if (!ts_lookup_visit_named(ctx->arena, state, recv_qn, depth))
return NULL;

const CBMRegisteredFunc *f =
cbm_registry_lookup_method_aliased(ctx->registry, recv_qn, method_name);
Expand All @@ -1853,8 +1952,11 @@ static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, recv_qn);
if (rt && rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *parent = cbm_type_named(ctx->arena, rt->embedded_types[i]);
const CBMRegisteredFunc *pf = lookup_method(ctx, parent, method_name);
const CBMRegisteredFunc *pf =
lookup_method_at(ctx, parent, method_name, depth + 1, state);
if (pf)
return pf;
}
Expand Down Expand Up @@ -3791,6 +3893,10 @@ void ts_lsp_init(TSLSPContext *ctx, CBMArena *arena, const char *source, int sou
bool jsx_mode, bool dts_mode, CBMResolvedCallArray *out) {
if (!ctx)
return;
#ifdef CBM_ENABLE_TEST_SEAMS
g_ts_max_member_lookup_steps = 0;
g_ts_max_method_lookup_steps = 0;
#endif
memset(ctx, 0, sizeof(TSLSPContext));
ctx->arena = arena;
ctx->source = source;
Expand Down
8 changes: 4 additions & 4 deletions internal/cbm/lsp/ts_lsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,17 @@ typedef struct {
// first completed eval; see TsEvalMemo in ts_lsp.c). Kills the exponential
// re-evaluation of shared subexpressions under overload resolution.
struct TsEvalMemo *eval_memo;
// Recursion guard for lookup_member_type: cyclic type graphs (mutually
// recursive unions/wrappers across registered types) otherwise recurse
// without bound — stack overflow on real repos.
int member_depth;
} TSLSPContext;

#ifdef CBM_ENABLE_TEST_SEAMS
// Complexity-regression seam: remaining expression-eval budget / warned flag
// for the calling thread, valid after a cbm_run_ts_lsp on the same thread.
long cbm_ts_lsp_test_budget_remaining(void);
bool cbm_ts_lsp_test_budget_warned(void);
// Maximum recursive calls in one member/method lookup during the last TS LSP run,
// including attempts rejected by the depth or visited-type guard.
int cbm_ts_lsp_test_max_member_lookup_steps(void);
int cbm_ts_lsp_test_max_method_lookup_steps(void);
#endif

// --- Initialization ---
Expand Down
Loading
Loading