Skip to content

Commit 80eb92a

Browse files
authored
Merge pull request #2252 from DeusData/distill/1245-scope-lookup
perf(lsp): collapse repeated Python scope-chain lookups
2 parents 9291014 + df8dd10 commit 80eb92a

4 files changed

Lines changed: 68 additions & 68 deletions

File tree

‎internal/cbm/lsp/py_lsp.c‎

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -191,20 +191,15 @@ static void py_disable_callable_value_proof(PyLSPContext *ctx) {
191191

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

199198
static void py_scope_bind_callable(PyLSPContext *ctx, const char *name, const CBMType *type,
200199
const char *callable_qn) {
201200
ctx->type_cache_gen++;
202-
cbm_scope_bind_callable(ctx->current_scope, name, type, callable_qn);
203-
const char *bound = name ? cbm_scope_lookup_callable(ctx->current_scope, name) : NULL;
204-
if (name && (!cbm_scope_contains(ctx->current_scope, name) ||
205-
(callable_qn && (!bound || strcmp(bound, callable_qn) != 0)))) {
201+
if (!cbm_scope_bind_callable_checked(ctx->current_scope, name, type, callable_qn))
206202
py_disable_callable_value_proof(ctx);
207-
}
208203
}
209204

210205
static CBMScope *py_scope_push_checked(PyLSPContext *ctx) {
@@ -938,8 +933,9 @@ static const char *py_exact_callable_target_ex(PyLSPContext *ctx, TSNode node,
938933
char *name = py_node_text(ctx, node);
939934
if (!name)
940935
return NULL;
941-
if (cbm_scope_contains(ctx->current_scope, name)) {
942-
const char *bound = cbm_scope_lookup_callable(ctx->current_scope, name);
936+
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, name);
937+
if (binding) {
938+
const char *bound = binding->callable_qn;
943939
if (bound && lexical_alias_out) {
944940
/* A lexical binding that does NOT simply name the module symbol
945941
* of the same spelling is an alias introduced in this body. Only
@@ -1018,11 +1014,12 @@ static void py_resolve_value_references_at(PyLSPContext *ctx, TSNode call) {
10181014
const char *candidate = strcmp(kind, "identifier") == 0
10191015
? py_exact_imported_reference_candidate(ctx, source_name)
10201016
: NULL;
1021-
if (candidate && cbm_scope_contains(ctx->current_scope, source_name)) {
1022-
const CBMType *binding =
1023-
cbm_type_resolve_alias(cbm_scope_lookup(ctx->current_scope, source_name));
1024-
if (binding && binding->kind == CBM_TYPE_NAMED && binding->data.named.qualified_name &&
1025-
strcmp(binding->data.named.qualified_name, candidate) == 0) {
1017+
const CBMVarBinding *scope_binding =
1018+
candidate ? cbm_scope_lookup_binding(ctx->current_scope, source_name) : NULL;
1019+
if (scope_binding) {
1020+
const CBMType *type = cbm_type_resolve_alias(scope_binding->type);
1021+
if (type && type->kind == CBM_TYPE_NAMED && type->data.named.qualified_name &&
1022+
strcmp(type->data.named.qualified_name, candidate) == 0) {
10261023
py_emit_unresolved_reference(ctx, candidate, arg);
10271024
}
10281025
}
@@ -1498,9 +1495,9 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
14981495
char *name = py_node_text(ctx, node);
14991496
if (!name)
15001497
return cbm_type_unknown();
1501-
const CBMType *t = cbm_scope_lookup(ctx->current_scope, name);
1502-
if (cbm_scope_contains(ctx->current_scope, name))
1503-
return t ? t : cbm_type_unknown();
1498+
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, name);
1499+
if (binding)
1500+
return binding->type ? binding->type : cbm_type_unknown();
15041501
// Builtin globals: True / False / None at top level.
15051502
if (strcmp(name, "True") == 0 || strcmp(name, "False") == 0)
15061503
return cbm_type_builtin(ctx->arena, "bool");
@@ -1767,8 +1764,9 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
17671764
}
17681765
}
17691766
// Constructor call: ClassName() returns NAMED(ClassName).
1770-
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
1771-
const char *callable_qn = cbm_scope_lookup_callable(ctx->current_scope, fname);
1767+
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, fname);
1768+
const CBMType *in_scope = binding && binding->type ? binding->type : cbm_type_unknown();
1769+
const char *callable_qn = binding ? binding->callable_qn : NULL;
17721770
if (callable_qn) {
17731771
return py_func_return_type(ctx, callable_qn);
17741772
}
@@ -1796,7 +1794,7 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
17961794
}
17971795
/* Even an UNKNOWN local/parameter is a real lexical shadow. Do
17981796
* not borrow the return type of a same-named module function. */
1799-
if (cbm_scope_contains(ctx->current_scope, fname))
1797+
if (binding)
18001798
return cbm_type_unknown();
18011799
// Module-local function call.
18021800
const CBMRegisteredFunc *f =
@@ -2566,14 +2564,15 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
25662564
* An ordinary local binding is also a hard shadow: if it is not a
25672565
* proven callable alias, do not fall through and fabricate a direct
25682566
* call to a module-level function with the same spelling. */
2569-
if (cbm_scope_contains(ctx->current_scope, fname)) {
2570-
const char *alias_target = cbm_scope_lookup_callable(ctx->current_scope, fname);
2567+
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, fname);
2568+
if (binding) {
2569+
const char *alias_target = binding->callable_qn;
25712570
if (alias_target) {
25722571
py_emit_resolved_call_reason(ctx, alias_target, "lsp_callable_alias", 0.97f, fname,
25732572
call_node);
25742573
return;
25752574
}
2576-
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
2575+
const CBMType *in_scope = binding->type ? binding->type : cbm_type_unknown();
25772576
if (!cbm_type_is_unknown(in_scope) && in_scope->kind == CBM_TYPE_NAMED) {
25782577
const char *qn = in_scope->data.named.qualified_name;
25792578
const char *tail = qn ? strrchr(qn, '.') : NULL;
@@ -2604,13 +2603,6 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
26042603
}
26052604
return;
26062605
}
2607-
// Constructor call (ClassName())
2608-
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
2609-
if (!cbm_type_is_unknown(in_scope) && in_scope->kind == CBM_TYPE_NAMED) {
2610-
const char *qn = in_scope->data.named.qualified_name;
2611-
py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f, call_node);
2612-
return;
2613-
}
26142606
// Module-local function
26152607
const CBMRegisteredFunc *f =
26162608
cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, fname);

‎internal/cbm/lsp/scope.c‎

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -89,51 +89,19 @@ bool cbm_scope_bind_callable_checked(CBMScope *scope, const char *name, const CB
8989
}
9090

9191
const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name) {
92-
if (!name) {
93-
return cbm_type_unknown();
94-
}
95-
for (const CBMScope* s = scope; s != NULL; s = s->parent) {
96-
for (CBMScopeChunk* c = s->chunks; c != NULL; c = c->next) {
97-
for (int i = 0; i < c->used; i++) {
98-
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
99-
return c->bindings[i].type;
100-
}
101-
}
102-
}
103-
}
92+
const CBMVarBinding *binding = cbm_scope_lookup_binding(scope, name);
93+
if (binding)
94+
return binding->type;
10495
return cbm_type_unknown();
10596
}
10697

10798
bool cbm_scope_contains(const CBMScope *scope, const char *name) {
108-
if (!name) {
109-
return false;
110-
}
111-
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
112-
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
113-
for (int i = 0; i < c->used; i++) {
114-
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
115-
return true;
116-
}
117-
}
118-
}
119-
}
120-
return false;
99+
return cbm_scope_lookup_binding(scope, name) != NULL;
121100
}
122101

123102
const char *cbm_scope_lookup_callable(const CBMScope *scope, const char *name) {
124-
if (!name) {
125-
return NULL;
126-
}
127-
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
128-
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
129-
for (int i = 0; i < c->used; i++) {
130-
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
131-
return c->bindings[i].callable_qn;
132-
}
133-
}
134-
}
135-
}
136-
return NULL;
103+
const CBMVarBinding *binding = cbm_scope_lookup_binding(scope, name);
104+
return binding ? binding->callable_qn : NULL;
137105
}
138106

139107
bool cbm_scope_update_callable(CBMScope *scope, const char *name, const char *callable_qn) {

‎internal/cbm/lsp/scope.h‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "../arena.h"
66
#include <stdatomic.h> /* relaxed cache for cbm_lsp_max_walk_depth */
77
#include <stdlib.h> /* getenv, atoi (cbm_lsp_max_walk_depth) */
8+
#include <string.h> /* strcmp (cbm_scope_lookup_binding) */
89

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

34+
/* Return the complete nearest binding in one scope-chain walk, or NULL when
35+
* unbound. Keep this internal hot-path primitive inline: all language
36+
* resolvers use cbm_scope_lookup(), while Python also consumes the complete
37+
* record to avoid repeating the same linear scan. */
38+
static inline const CBMVarBinding *cbm_scope_lookup_binding(const CBMScope *scope,
39+
const char *name) {
40+
if (!name)
41+
return NULL;
42+
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
43+
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
44+
for (int i = 0; i < c->used; i++) {
45+
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0)
46+
return &c->bindings[i];
47+
}
48+
}
49+
}
50+
return NULL;
51+
}
52+
3353
// Bail-to-UNKNOWN depth for type-lookup chains: alias resolution, MRO walks,
3454
// embedded-field/struct-traversal. Exceeding this collapses to cbm_type_unknown
3555
// rather than recursing — guards against pathological hierarchies.

‎tests/test_scope.c‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,25 @@ TEST(scope_callable_identity_follows_nearest_binding) {
125125
PASS();
126126
}
127127

128+
TEST(scope_lookup_binding_returns_nearest_complete_record) {
129+
CBMArena a;
130+
cbm_arena_init(&a);
131+
CBMScope *root = cbm_scope_push(&a, NULL);
132+
CBMScope *child = cbm_scope_push(&a, root);
133+
cbm_scope_bind_callable(root, "callback", named_t(&a, "Callback"), "pkg.actual");
134+
cbm_scope_bind(child, "callback", named_t(&a, "Shadow"));
135+
136+
const CBMVarBinding *binding = cbm_scope_lookup_binding(child, "callback");
137+
ASSERT_NOT_NULL(binding);
138+
ASSERT_STR_EQ(binding->name, "callback");
139+
ASSERT_STR_EQ(binding->type->data.named.qualified_name, "Shadow");
140+
ASSERT_NULL(binding->callable_qn);
141+
ASSERT_NULL(cbm_scope_lookup_binding(child, "missing"));
142+
143+
cbm_arena_destroy(&a);
144+
PASS();
145+
}
146+
128147
TEST(scope_assignment_updates_or_clears_nearest_callable_only) {
129148
CBMArena a;
130149
cbm_arena_init(&a);
@@ -260,6 +279,7 @@ SUITE(scope) {
260279
RUN_TEST(scope_lookup_walks_parent_chain);
261280
RUN_TEST(scope_child_shadows_parent);
262281
RUN_TEST(scope_callable_identity_follows_nearest_binding);
282+
RUN_TEST(scope_lookup_binding_returns_nearest_complete_record);
263283
RUN_TEST(scope_assignment_updates_or_clears_nearest_callable_only);
264284
RUN_TEST(scope_checked_bind_reports_child_oom_despite_parent_name);
265285
RUN_TEST(scope_dynamic_growth_300_bindings);

0 commit comments

Comments
 (0)