Skip to content

Commit 1160fa3

Browse files
authored
Merge pull request DeusData#2274 from DeusData/distill/1245-c-pointer-return-types
fix(extract): a C-family function's return type keeps its pointer and qualifiers
2 parents b0691e0 + 343d7dd commit 1160fa3

2 files changed

Lines changed: 253 additions & 2 deletions

File tree

‎internal/cbm/extract_defs.c‎

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3477,6 +3477,159 @@ static TSNode find_function_params(TSNode func_node, CBMLanguage lang) {
34773477
return params;
34783478
}
34793479

3480+
/* ── C-family declared return type ──────────────────────────────────
3481+
* The C-family grammars split a declared return type three ways: the `type`
3482+
* field (`char`), sibling type_qualifier nodes (`const`), and the
3483+
* pointer/reference declarators wrapping the function declarator (`*`). Taking
3484+
* only the `type` field published `const char *get_name(void)` as "char".
3485+
*
3486+
* Canonical spelling: leading cv-qualifiers in source order, the base type text
3487+
* verbatim, then one space and the declarator markers outermost-first with no
3488+
* space between them — `const char *`, `char **`, `Text &`, `Text *&`. A
3489+
* qualifier on a pointer level follows its `*` and is separated from the next
3490+
* marker by a space: `char *const *`. A qualifier written after the base type
3491+
* (`char const *`) is normalized to the leading position. */
3492+
3493+
/* Output sink: measures when buf is NULL, writes otherwise. Rendering twice
3494+
* sizes the arena allocation exactly without a second copy of the logic. */
3495+
typedef struct {
3496+
char *buf;
3497+
size_t len;
3498+
} c_rt_out_t;
3499+
3500+
static void c_rt_put(c_rt_out_t *out, const char *text, size_t n) {
3501+
if (out->buf) {
3502+
memcpy(out->buf + out->len, text, n);
3503+
}
3504+
out->len += n;
3505+
}
3506+
3507+
static void c_rt_put_str(c_rt_out_t *out, const char *text) {
3508+
c_rt_put(out, text, strlen(text));
3509+
}
3510+
3511+
static void c_rt_put_node(c_rt_out_t *out, TSNode node, const char *source) {
3512+
uint32_t start = ts_node_start_byte(node);
3513+
uint32_t end = ts_node_end_byte(node);
3514+
if (end > start) {
3515+
c_rt_put(out, source + start, (size_t)(end - start));
3516+
}
3517+
}
3518+
3519+
/* type_qualifier also covers keywords that are not part of the type
3520+
* (`constexpr`, `_Noreturn`, `mutable`, `__extension__`, …). Keep only the ones
3521+
* that are, so `constexpr int f()` still returns "int". */
3522+
static bool is_c_return_cv_qualifier(TSNode node, const char *source) {
3523+
static const char *const kept[] = {"const", "volatile", "restrict", "__restrict",
3524+
"__restrict__", "_Atomic", NULL};
3525+
if (strcmp(ts_node_type(node), "type_qualifier") != 0) {
3526+
return false;
3527+
}
3528+
uint32_t start = ts_node_start_byte(node);
3529+
uint32_t end = ts_node_end_byte(node);
3530+
size_t len = end > start ? (size_t)(end - start) : 0;
3531+
for (const char *const *k = kept; *k; k++) {
3532+
if (strlen(*k) == len && memcmp(source + start, *k, len) == 0) {
3533+
return true;
3534+
}
3535+
}
3536+
return false;
3537+
}
3538+
3539+
static bool is_c_declarator_lang(CBMLanguage lang) {
3540+
return lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
3541+
lang == CBM_LANG_GLSL || lang == CBM_LANG_HLSL || lang == CBM_LANG_ISPC ||
3542+
lang == CBM_LANG_SLANG || lang == CBM_LANG_OBJC;
3543+
}
3544+
3545+
/* Render the canonical return type into `out`; returns how many qualifiers and
3546+
* markers were added around the base type (0 = the base text alone is already
3547+
* the whole type). The declarator walk is one strict child chain, so it is
3548+
* O(depth) with no recursion and needs no depth cap. It stops at the first node
3549+
* that is neither a pointer nor a reference declarator: for a function returning
3550+
* a function pointer (`int (*f(void))(int)`) that is the outer
3551+
* function_declarator, which leaves the base type as it was. */
3552+
static size_t c_rt_render(c_rt_out_t *out, TSNode func_node, TSNode type_node, TSNode declarator,
3553+
const char *source) {
3554+
size_t added = 0;
3555+
uint32_t decl_start = ts_node_start_byte(declarator);
3556+
uint32_t nc = ts_node_named_child_count(func_node);
3557+
for (uint32_t i = 0; i < nc; i++) {
3558+
TSNode ch = ts_node_named_child(func_node, i);
3559+
if (ts_node_start_byte(ch) >= decl_start) {
3560+
break;
3561+
}
3562+
if (is_c_return_cv_qualifier(ch, source)) {
3563+
c_rt_put_node(out, ch, source);
3564+
c_rt_put_str(out, " ");
3565+
added++;
3566+
}
3567+
}
3568+
c_rt_put_node(out, type_node, source);
3569+
3570+
bool need_space = true;
3571+
TSNode decl = declarator;
3572+
while (!ts_node_is_null(decl)) {
3573+
const char *dk = ts_node_type(decl);
3574+
bool is_ref = strcmp(dk, "reference_declarator") == 0;
3575+
if (!is_ref && strcmp(dk, "pointer_declarator") != 0) {
3576+
break;
3577+
}
3578+
if (need_space) {
3579+
c_rt_put_str(out, " ");
3580+
need_space = false;
3581+
}
3582+
/* A reference_declarator opens with its `&` / `&&` token. */
3583+
TSNode marker = ts_node_child(decl, 0);
3584+
if (is_ref && !ts_node_is_null(marker) && !ts_node_is_named(marker)) {
3585+
c_rt_put_node(out, marker, source);
3586+
} else {
3587+
c_rt_put_str(out, is_ref ? "&" : "*");
3588+
}
3589+
added++;
3590+
uint32_t dn = ts_node_named_child_count(decl);
3591+
for (uint32_t i = 0; i < dn; i++) {
3592+
TSNode q = ts_node_named_child(decl, i);
3593+
if (is_c_return_cv_qualifier(q, source)) {
3594+
c_rt_put_node(out, q, source);
3595+
need_space = true;
3596+
added++;
3597+
}
3598+
}
3599+
/* tree-sitter-cpp/-cuda give a reference_declarator's inner declarator no
3600+
* `declarator` field (see find_c_params); it is the one named child. */
3601+
TSNode inner = ts_node_child_by_field_name(decl, TS_FIELD("declarator"));
3602+
if (ts_node_is_null(inner) && is_ref && dn > 0) {
3603+
inner = ts_node_named_child(decl, 0);
3604+
}
3605+
decl = inner;
3606+
}
3607+
return added;
3608+
}
3609+
3610+
/* Declared return type of a C-family function/method node whose `type` field is
3611+
* `type_node`. Any other language, and any type with nothing around its base
3612+
* type, gets the base type text exactly as before. */
3613+
static char *c_declared_return_type(CBMExtractCtx *ctx, TSNode func_node, TSNode type_node) {
3614+
CBMArena *a = ctx->arena;
3615+
TSNode declarator = ts_node_child_by_field_name(func_node, TS_FIELD("declarator"));
3616+
if (!is_c_declarator_lang(ctx->language) || ts_node_is_null(declarator)) {
3617+
return cbm_node_text(a, type_node, ctx->source);
3618+
}
3619+
c_rt_out_t out = {NULL, 0};
3620+
if (c_rt_render(&out, func_node, type_node, declarator, ctx->source) == 0) {
3621+
return cbm_node_text(a, type_node, ctx->source);
3622+
}
3623+
out.buf = (char *)cbm_arena_alloc(a, out.len + NULL_TERM);
3624+
if (!out.buf) {
3625+
return cbm_node_text(a, type_node, ctx->source);
3626+
}
3627+
out.len = 0;
3628+
(void)c_rt_render(&out, func_node, type_node, declarator, ctx->source);
3629+
out.buf[out.len] = '\0';
3630+
return out.buf;
3631+
}
3632+
34803633
// C++: resolve trailing return type (auto f() -> Type) on a declarator node.
34813634
// Updates def->return_type and def->return_types if trailing type found.
34823635
static void resolve_cpp_trailing_return(CBMArena *a, TSNode func_node, const char *source,
@@ -3712,7 +3865,7 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
37123865
for (const char **f = rt_fields; *f; f++) {
37133866
TSNode rt = ts_node_child_by_field_name(func_node, *f, (uint32_t)strlen(*f));
37143867
if (!ts_node_is_null(rt)) {
3715-
def.return_type = cbm_node_text(a, rt, ctx->source);
3868+
def.return_type = c_declared_return_type(ctx, func_node, rt);
37163869
def.return_types = extract_return_types(a, rt, ctx->source, ctx->language);
37173870
break;
37183871
}
@@ -4964,7 +5117,7 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,
49645117
for (const char **f = rt_fields; *f; f++) {
49655118
TSNode rt = ts_node_child_by_field_name(child, *f, (uint32_t)strlen(*f));
49665119
if (!ts_node_is_null(rt)) {
4967-
def.return_type = cbm_node_text(a, rt, ctx->source);
5120+
def.return_type = c_declared_return_type(ctx, child, rt);
49685121
break;
49695122
}
49705123
}

‎tests/test_extraction.c‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,65 @@ TEST(c_struct) {
977977
PASS();
978978
}
979979

980+
/* return_type of the first definition named `name`; NULL when there is no such
981+
* definition or it carries no return type. */
982+
static const char *def_return_type(CBMFileResult *r, const char *name) {
983+
for (int i = 0; i < r->defs.count; i++) {
984+
if (strcmp(r->defs.items[i].name, name) == 0) {
985+
return r->defs.items[i].return_type;
986+
}
987+
}
988+
return NULL;
989+
}
990+
991+
/* PR #1245: the C grammar splits a declared return type across the `type` node,
992+
* sibling type_qualifier nodes and the pointer_declarator chain wrapping the
993+
* function declarator. Taking only the `type` node published `const char *` as
994+
* "char". Canonical spelling: qualifiers, base type, one space, then the
995+
* declarator markers unspaced (`const char *`, `char **`). */
996+
TEST(c_function_return_type_preserves_pointer_and_qualifier) {
997+
CBMFileResult *r = extract("static const char *text_end(const char *text) { return text; }\n"
998+
"char **table(void) { return 0; }\n"
999+
"char const *east_const(void) { return 0; }\n"
1000+
"const struct Point *find_point(void) { return 0; }\n"
1001+
"volatile unsigned long *reg(void) { return 0; }\n"
1002+
"char *const *frozen(void) { return 0; }\n",
1003+
CBM_LANG_C, "t", "returns.c");
1004+
ASSERT_NOT_NULL(r);
1005+
ASSERT_FALSE(r->has_error);
1006+
ASSERT_STR_EQ(def_return_type(r, "text_end"), "const char *");
1007+
ASSERT_STR_EQ(def_return_type(r, "table"), "char **");
1008+
ASSERT_STR_EQ(def_return_type(r, "east_const"), "const char *");
1009+
ASSERT_STR_EQ(def_return_type(r, "find_point"), "const struct Point *");
1010+
ASSERT_STR_EQ(def_return_type(r, "reg"), "volatile unsigned long *");
1011+
ASSERT_STR_EQ(def_return_type(r, "frozen"), "char *const *");
1012+
cbm_free_result(r);
1013+
PASS();
1014+
}
1015+
1016+
/* Guard for the fix above: a return type with no qualifier and no declarator
1017+
* marker is already correct and must come out byte-identical. */
1018+
TEST(c_function_return_type_plain_unchanged) {
1019+
CBMFileResult *r = extract("int scalar(void) { return 0; }\n"
1020+
"void nothing(void) {}\n"
1021+
"unsigned long wide(void) { return 0; }\n"
1022+
"struct Point make_point(void) { struct Point p; return p; }\n"
1023+
"static inline size_t count(void) { return 0; }\n"
1024+
"_Noreturn void die(void) { for (;;) {} }\n",
1025+
CBM_LANG_C, "t", "plain.c");
1026+
ASSERT_NOT_NULL(r);
1027+
ASSERT_FALSE(r->has_error);
1028+
ASSERT_STR_EQ(def_return_type(r, "scalar"), "int");
1029+
ASSERT_STR_EQ(def_return_type(r, "nothing"), "void");
1030+
ASSERT_STR_EQ(def_return_type(r, "wide"), "unsigned long");
1031+
ASSERT_STR_EQ(def_return_type(r, "make_point"), "struct Point");
1032+
ASSERT_STR_EQ(def_return_type(r, "count"), "size_t");
1033+
/* _Noreturn parses as a type_qualifier but is not part of the type. */
1034+
ASSERT_STR_EQ(def_return_type(r, "die"), "void");
1035+
cbm_free_result(r);
1036+
PASS();
1037+
}
1038+
9801039
/* --- C++ --- */
9811040
TEST(cpp_class) {
9821041
CBMFileResult *r = extract(
@@ -990,6 +1049,42 @@ TEST(cpp_class) {
9901049
PASS();
9911050
}
9921051

1052+
/* PR #1245, C++ side: in-class methods go through a separate extraction path
1053+
* from free functions, and C++ adds reference markers (`&`, `&&`) whose
1054+
* reference_declarator carries no `declarator` field. */
1055+
TEST(cpp_method_return_type_preserves_pointer_and_qualifier) {
1056+
CBMFileResult *r = extract("class Text {\n"
1057+
"public:\n"
1058+
" const char *end() { return nullptr; }\n"
1059+
" char **table() { return nullptr; }\n"
1060+
" Text &self() { return *this; }\n"
1061+
" const Text &cself() const { return *this; }\n"
1062+
" Text *&slot() { return next_; }\n"
1063+
" Text *next_;\n"
1064+
" int width() const { return 0; }\n"
1065+
" constexpr int square(int x) const { return x * x; }\n"
1066+
"};\n"
1067+
"const Text &shared() { static Text t; return t; }\n"
1068+
"Text &&moved(Text &t) { return static_cast<Text &&>(t); }\n"
1069+
"const char *Text::c_str() const { return nullptr; }\n",
1070+
CBM_LANG_CPP, "t", "text.cpp");
1071+
ASSERT_NOT_NULL(r);
1072+
ASSERT_FALSE(r->has_error);
1073+
ASSERT_STR_EQ(def_return_type(r, "end"), "const char *");
1074+
ASSERT_STR_EQ(def_return_type(r, "table"), "char **");
1075+
ASSERT_STR_EQ(def_return_type(r, "self"), "Text &");
1076+
ASSERT_STR_EQ(def_return_type(r, "cself"), "const Text &");
1077+
ASSERT_STR_EQ(def_return_type(r, "slot"), "Text *&");
1078+
ASSERT_STR_EQ(def_return_type(r, "width"), "int");
1079+
/* constexpr parses as a type_qualifier but is not part of the type. */
1080+
ASSERT_STR_EQ(def_return_type(r, "square"), "int");
1081+
ASSERT_STR_EQ(def_return_type(r, "shared"), "const Text &");
1082+
ASSERT_STR_EQ(def_return_type(r, "moved"), "Text &&");
1083+
ASSERT_STR_EQ(def_return_type(r, "c_str"), "const char *");
1084+
cbm_free_result(r);
1085+
PASS();
1086+
}
1087+
9931088
/* ═══════════════════════════════════════════════════════════════════
9941089
* Group C: Scripting / Dynamic Languages
9951090
* ═══════════════════════════════════════════════════════════════════ */
@@ -8271,8 +8366,11 @@ SUITE(extraction) {
82718366
RUN_TEST(go_interface);
82728367
RUN_TEST(zig_function);
82738368
RUN_TEST(c_function);
8369+
RUN_TEST(c_function_return_type_preserves_pointer_and_qualifier);
8370+
RUN_TEST(c_function_return_type_plain_unchanged);
82748371
RUN_TEST(c_struct);
82758372
RUN_TEST(cpp_class);
8373+
RUN_TEST(cpp_method_return_type_preserves_pointer_and_qualifier);
82768374

82778375
/* Scripting */
82788376
RUN_TEST(python_function);

0 commit comments

Comments
 (0)