feat(py_lsp): decorator flags + verifications for generators / dataclasses / properties

Round 6:

CBMRegisteredFunc gains a `flags` bitfield (CBMFuncFlags) and a
`decorator_qns` NULL-terminated list. py_register_def populates them
from CBMDefinition.decorators by short-name match — so `@property`,
`@functools.cache`, `@abc.abstractmethod`, `@typing.overload`, and
`@final` all light up regardless of qualified vs bare reference. The
flags aren't consumed by resolution yet (still on roadmap: emit
distinct edge for @property attribute access vs method call); they
are recorded so future overload selection / property modeling can
use them without another schema change.

The struct extension is at-tail and existing call sites that memset
to zero stay correct; non-Python LSPs see flags=0 / decorator_qns=NULL
and behave identically.

Three new verification tests prove that generators, dataclasses, and
@property already work end-to-end through the Round 4-5 infrastructure
without further code changes:
- Generator[Foo, ...] iterables in for-loop bind loop var to Foo via
  py_iterable_element_type's existing TEMPLATE handling.
- @dataclass class Point: x: Foo (class-body annotated assignment) is
  picked up by Round 4's third-pass field registrar; p.x.method()
  resolves through the field type.
- @property def thing(self) -> Foo: c.thing returns Foo because
  attribute access on NAMED routes through py_func_return_type which
  yields the getter's annotated return.

All 2895 prior tests stay green; 3 new tests pass. Total 2898 / 0.
Bench unchanged at 95% (the small bench fixture doesn't exercise
the new patterns; will expand in Round 7).
This commit is contained in:
Martin Vogel committed 2026-05-09 10:02:12 +02:00
1 parent ce54e817b7
commit fd81dffc8f
3 files changed
+114

No files matched your search

+33
View File
@@ -1919,6 +1919,39 @@ static bool py_register_def(CBMArena* arena, CBMTypeRegistry* reg, CBMDefinition
rf.qualified_name = d->qualified_name;
rf.short_name = d->name;
// Translate Python decorators into flags + carry the QN list.
if (d->decorators) {
int dec_count = 0;
while (d->decorators[dec_count]) dec_count++;
if (dec_count > 0) {
const char** dec_qns = (const char**)cbm_arena_alloc(arena,
(size_t)(dec_count + 1) * sizeof(const char*));
for (int j = 0; j < dec_count; j++) {
const char* dec = d->decorators[j];
dec_qns[j] = cbm_arena_strdup(arena, dec);
// Match by short name (last "." segment) so both
// "property" and "functools.cache" / "abc.abstractmethod"
// forms light up.
const char* short_dec = strrchr(dec, '.');
short_dec = short_dec ? short_dec + 1 : dec;
if (strcmp(short_dec, "property") == 0)
rf.flags |= CBM_FUNC_FLAG_PROPERTY;
else if (strcmp(short_dec, "classmethod") == 0)
rf.flags |= CBM_FUNC_FLAG_CLASSMETHOD;
else if (strcmp(short_dec, "staticmethod") == 0)
rf.flags |= CBM_FUNC_FLAG_STATICMETHOD;
else if (strcmp(short_dec, "abstractmethod") == 0)
rf.flags |= CBM_FUNC_FLAG_ABSTRACTMETHOD;
else if (strcmp(short_dec, "overload") == 0)
rf.flags |= CBM_FUNC_FLAG_OVERLOAD;
else if (strcmp(short_dec, "final") == 0)
rf.flags |= CBM_FUNC_FLAG_FINAL;
}
dec_qns[dec_count] = NULL;
rf.decorator_qns = dec_qns;
}
}
const CBMType** ret_types = NULL;
// Prefer d->return_type (full text) when it has subscript brackets
// — extract_defs.c::extract_return_types strips them in its array
+17
View File
@@ -4,6 +4,20 @@
#include "type_rep.h"
#include "../arena.h"
// Decorator-derived flags (Python). Added at struct tail so existing
// callers that memset to zero before populating other fields keep working.
typedef enum {
CBM_FUNC_FLAG_NONE = 0,
CBM_FUNC_FLAG_PROPERTY = 1 << 0, // @property -> obj.attr returns getter return
CBM_FUNC_FLAG_CLASSMETHOD = 1 << 1, // @classmethod -> first arg is cls (the class)
CBM_FUNC_FLAG_STATICMETHOD = 1 << 2, // @staticmethod -> no implicit self/cls
CBM_FUNC_FLAG_ABSTRACTMETHOD = 1 << 3, // @abstractmethod -> still callable for resolution
CBM_FUNC_FLAG_OVERLOAD = 1 << 4, // @overload entry — non-implementation stub
CBM_FUNC_FLAG_ASYNC = 1 << 5, // async def — return is Coroutine[..., T]
CBM_FUNC_FLAG_GENERATOR = 1 << 6, // contains yield — return is Generator[T, ...]
CBM_FUNC_FLAG_FINAL = 1 << 7, // @final — overrides not allowed
} CBMFuncFlags;
// Registered function/method with full type signature.
typedef struct {
const char* qualified_name; // e.g., "proj.pkg.TypeName.MethodName"
@@ -12,6 +26,9 @@ typedef struct {
const CBMType* signature; // FUNC type with param/return types
const char** type_param_names; // NULL-terminated, e.g., ["T", "R", NULL] for generics
int min_params; // Minimum required params (excluding defaulted). -1 = unknown.
int flags; // CBM_FUNC_FLAG_* bitfield (Python decorator info; 0 elsewhere)
const char** decorator_qns; // NULL-terminated decorator QNs (Python only); used for
// user-decorator return-type substitution.
} CBMRegisteredFunc;
// Registered type with fields and method names.
+64
View File
@@ -1145,6 +1145,66 @@ TEST(pylsp_round5_super_init) {
PASS();
}
/* ── Round 6 — generators, dataclasses, decorator flags ───────── */
TEST(pylsp_round6_generator_yields_iterable) {
/* def gen() -> Generator[Foo, None, None]: yield Foo()
* for x in gen(): x.method() — x : Foo via element-of(generator) */
CBMFileResult *r = extract_py(
"from typing import Generator\n"
"class Foo:\n"
" def method(self):\n"
" return 1\n"
"def gen() -> Generator[Foo, None, None]:\n"
" yield Foo()\n"
"def use():\n"
" for x in gen():\n"
" x.method()\n");
ASSERT_NOT_NULL(r);
ASSERT_GTE(require_resolved(r, "use", "method"), 0);
cbm_free_result(r);
PASS();
}
TEST(pylsp_round6_dataclass_field_access) {
/* @dataclass class Point: x: Foo; def use(p: Point): p.x.method() */
CBMFileResult *r = extract_py(
"from dataclasses import dataclass\n"
"class Foo:\n"
" def method(self):\n"
" return 1\n"
"@dataclass\n"
"class Point:\n"
" x: Foo\n"
" y: int\n"
"def use(p: Point):\n"
" return p.x.method()\n");
ASSERT_NOT_NULL(r);
ASSERT_GTE(require_resolved(r, "use", "method"), 0);
cbm_free_result(r);
PASS();
}
TEST(pylsp_round6_property_access_chains) {
/* class C: @property def thing(self) -> Foo: ...
* def use(c: C): c.thing.method() -- thing is a property; access
* returns its getter's return type. */
CBMFileResult *r = extract_py(
"class Foo:\n"
" def method(self):\n"
" return 1\n"
"class C:\n"
" @property\n"
" def thing(self) -> Foo:\n"
" return Foo()\n"
"def use(c: C):\n"
" return c.thing.method()\n");
ASSERT_NOT_NULL(r);
ASSERT_GTE(require_resolved(r, "use", "method"), 0);
cbm_free_result(r);
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────── */
SUITE(py_lsp) {
@@ -1215,4 +1275,8 @@ SUITE(py_lsp) {
RUN_TEST(pylsp_round5_dict_subscript_value_type);
RUN_TEST(pylsp_round5_list_subscript_value_type);
RUN_TEST(pylsp_round5_super_init);
/* Round 6 — generators, dataclasses, properties */
RUN_TEST(pylsp_round6_generator_yields_iterable);
RUN_TEST(pylsp_round6_dataclass_field_access);
RUN_TEST(pylsp_round6_property_access_chains);
}