Skip to content
Merged
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
52 changes: 22 additions & 30 deletions internal/cbm/lsp/py_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -191,20 +191,15 @@ static void py_disable_callable_value_proof(PyLSPContext *ctx) {

static void py_scope_bind(PyLSPContext *ctx, const char *name, const CBMType *type) {
ctx->type_cache_gen++;
cbm_scope_bind(ctx->current_scope, name, type);
if (name && !cbm_scope_contains(ctx->current_scope, name))
if (!cbm_scope_bind_checked(ctx->current_scope, name, type))
py_disable_callable_value_proof(ctx);
}

static void py_scope_bind_callable(PyLSPContext *ctx, const char *name, const CBMType *type,
const char *callable_qn) {
ctx->type_cache_gen++;
cbm_scope_bind_callable(ctx->current_scope, name, type, callable_qn);
const char *bound = name ? cbm_scope_lookup_callable(ctx->current_scope, name) : NULL;
if (name && (!cbm_scope_contains(ctx->current_scope, name) ||
(callable_qn && (!bound || strcmp(bound, callable_qn) != 0)))) {
if (!cbm_scope_bind_callable_checked(ctx->current_scope, name, type, callable_qn))
py_disable_callable_value_proof(ctx);
}
}

static CBMScope *py_scope_push_checked(PyLSPContext *ctx) {
Expand Down Expand Up @@ -938,8 +933,9 @@ static const char *py_exact_callable_target_ex(PyLSPContext *ctx, TSNode node,
char *name = py_node_text(ctx, node);
if (!name)
return NULL;
if (cbm_scope_contains(ctx->current_scope, name)) {
const char *bound = cbm_scope_lookup_callable(ctx->current_scope, name);
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, name);
if (binding) {
const char *bound = binding->callable_qn;
if (bound && lexical_alias_out) {
/* A lexical binding that does NOT simply name the module symbol
* of the same spelling is an alias introduced in this body. Only
Expand Down Expand Up @@ -1018,11 +1014,12 @@ static void py_resolve_value_references_at(PyLSPContext *ctx, TSNode call) {
const char *candidate = strcmp(kind, "identifier") == 0
? py_exact_imported_reference_candidate(ctx, source_name)
: NULL;
if (candidate && cbm_scope_contains(ctx->current_scope, source_name)) {
const CBMType *binding =
cbm_type_resolve_alias(cbm_scope_lookup(ctx->current_scope, source_name));
if (binding && binding->kind == CBM_TYPE_NAMED && binding->data.named.qualified_name &&
strcmp(binding->data.named.qualified_name, candidate) == 0) {
const CBMVarBinding *scope_binding =
candidate ? cbm_scope_lookup_binding(ctx->current_scope, source_name) : NULL;
if (scope_binding) {
const CBMType *type = cbm_type_resolve_alias(scope_binding->type);
if (type && type->kind == CBM_TYPE_NAMED && type->data.named.qualified_name &&
strcmp(type->data.named.qualified_name, candidate) == 0) {
py_emit_unresolved_reference(ctx, candidate, arg);
}
}
Expand Down Expand Up @@ -1498,9 +1495,9 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
char *name = py_node_text(ctx, node);
if (!name)
return cbm_type_unknown();
const CBMType *t = cbm_scope_lookup(ctx->current_scope, name);
if (cbm_scope_contains(ctx->current_scope, name))
return t ? t : cbm_type_unknown();
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, name);
if (binding)
return binding->type ? binding->type : cbm_type_unknown();
// Builtin globals: True / False / None at top level.
if (strcmp(name, "True") == 0 || strcmp(name, "False") == 0)
return cbm_type_builtin(ctx->arena, "bool");
Expand Down Expand Up @@ -1767,8 +1764,9 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
}
}
// Constructor call: ClassName() returns NAMED(ClassName).
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
const char *callable_qn = cbm_scope_lookup_callable(ctx->current_scope, fname);
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, fname);
const CBMType *in_scope = binding && binding->type ? binding->type : cbm_type_unknown();
const char *callable_qn = binding ? binding->callable_qn : NULL;
if (callable_qn) {
return py_func_return_type(ctx, callable_qn);
}
Expand Down Expand Up @@ -1796,7 +1794,7 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
}
/* Even an UNKNOWN local/parameter is a real lexical shadow. Do
* not borrow the return type of a same-named module function. */
if (cbm_scope_contains(ctx->current_scope, fname))
if (binding)
return cbm_type_unknown();
// Module-local function call.
const CBMRegisteredFunc *f =
Expand Down Expand Up @@ -2566,14 +2564,15 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
* An ordinary local binding is also a hard shadow: if it is not a
* proven callable alias, do not fall through and fabricate a direct
* call to a module-level function with the same spelling. */
if (cbm_scope_contains(ctx->current_scope, fname)) {
const char *alias_target = cbm_scope_lookup_callable(ctx->current_scope, fname);
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, fname);
if (binding) {
const char *alias_target = binding->callable_qn;
if (alias_target) {
py_emit_resolved_call_reason(ctx, alias_target, "lsp_callable_alias", 0.97f, fname,
call_node);
return;
}
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
const CBMType *in_scope = binding->type ? binding->type : cbm_type_unknown();
if (!cbm_type_is_unknown(in_scope) && in_scope->kind == CBM_TYPE_NAMED) {
const char *qn = in_scope->data.named.qualified_name;
const char *tail = qn ? strrchr(qn, '.') : NULL;
Expand Down Expand Up @@ -2604,13 +2603,6 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
}
return;
}
// Constructor call (ClassName())
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
if (!cbm_type_is_unknown(in_scope) && in_scope->kind == CBM_TYPE_NAMED) {
const char *qn = in_scope->data.named.qualified_name;
py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f, call_node);
return;
}
// Module-local function
const CBMRegisteredFunc *f =
cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, fname);
Expand Down
44 changes: 6 additions & 38 deletions internal/cbm/lsp/scope.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,51 +89,19 @@ bool cbm_scope_bind_callable_checked(CBMScope *scope, const char *name, const CB
}

const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name) {
if (!name) {
return cbm_type_unknown();
}
for (const 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) {
return c->bindings[i].type;
}
}
}
}
const CBMVarBinding *binding = cbm_scope_lookup_binding(scope, name);
if (binding)
return binding->type;
return cbm_type_unknown();
}

bool cbm_scope_contains(const CBMScope *scope, const char *name) {
if (!name) {
return false;
}
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 true;
}
}
}
}
return false;
return cbm_scope_lookup_binding(scope, name) != NULL;
}

const char *cbm_scope_lookup_callable(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].callable_qn;
}
}
}
}
return NULL;
const CBMVarBinding *binding = cbm_scope_lookup_binding(scope, name);
return binding ? binding->callable_qn : NULL;
}

bool cbm_scope_update_callable(CBMScope *scope, const char *name, const char *callable_qn) {
Expand Down
20 changes: 20 additions & 0 deletions internal/cbm/lsp/scope.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "../arena.h"
#include <stdatomic.h> /* relaxed cache for cbm_lsp_max_walk_depth */
#include <stdlib.h> /* getenv, atoi (cbm_lsp_max_walk_depth) */
#include <string.h> /* strcmp (cbm_scope_lookup_binding) */

typedef struct {
const char* name;
Expand All @@ -30,6 +31,25 @@ typedef struct CBMScope {
CBMArena* arena; // owning arena, propagated to children at push time
} CBMScope;

/* 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. */
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];
}
}
}
return NULL;
}

// Bail-to-UNKNOWN depth for type-lookup chains: alias resolution, MRO walks,
// embedded-field/struct-traversal. Exceeding this collapses to cbm_type_unknown
// rather than recursing — guards against pathological hierarchies.
Expand Down
20 changes: 20 additions & 0 deletions tests/test_scope.c
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,25 @@ TEST(scope_callable_identity_follows_nearest_binding) {
PASS();
}

TEST(scope_lookup_binding_returns_nearest_complete_record) {
CBMArena a;
cbm_arena_init(&a);
CBMScope *root = cbm_scope_push(&a, NULL);
CBMScope *child = cbm_scope_push(&a, root);
cbm_scope_bind_callable(root, "callback", named_t(&a, "Callback"), "pkg.actual");
cbm_scope_bind(child, "callback", named_t(&a, "Shadow"));

const CBMVarBinding *binding = cbm_scope_lookup_binding(child, "callback");
ASSERT_NOT_NULL(binding);
ASSERT_STR_EQ(binding->name, "callback");
ASSERT_STR_EQ(binding->type->data.named.qualified_name, "Shadow");
ASSERT_NULL(binding->callable_qn);
ASSERT_NULL(cbm_scope_lookup_binding(child, "missing"));

cbm_arena_destroy(&a);
PASS();
}

TEST(scope_assignment_updates_or_clears_nearest_callable_only) {
CBMArena a;
cbm_arena_init(&a);
Expand Down Expand Up @@ -260,6 +279,7 @@ SUITE(scope) {
RUN_TEST(scope_lookup_walks_parent_chain);
RUN_TEST(scope_child_shadows_parent);
RUN_TEST(scope_callable_identity_follows_nearest_binding);
RUN_TEST(scope_lookup_binding_returns_nearest_complete_record);
RUN_TEST(scope_assignment_updates_or_clears_nearest_callable_only);
RUN_TEST(scope_checked_bind_reports_child_oom_despite_parent_name);
RUN_TEST(scope_dynamic_growth_300_bindings);
Expand Down
Loading