Skip to content

Commit f0cf84d

Browse files
authored
Merge pull request #1426 from tthayer/feat/pkl-call-extraction
feat(pkl): extract calls, branches, throws and decorators
2 parents a824d82 + c9d68de commit f0cf84d

8 files changed

Lines changed: 212 additions & 64 deletions

‎internal/cbm/extract_calls.c‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,65 @@ static char *extract_nickel_callee(CBMArena *a, TSNode node, const char *source,
11611161
return NULL;
11621162
}
11631163

1164+
// Pkl: `unqualifiedAccessExpr` / `qualifiedAccessExpr` are the same node whether
1165+
// they are a call (`helper(a)`) or a bare property read (`host`) — the only
1166+
// discriminator is an `argumentList` child, so both are gated on it. For a
1167+
// qualified call the method name is the `identifier` child that is not the
1168+
// `receiver`; the receiver is prefixed only when it is itself a plain name
1169+
// (`utils.fallback(a)` -> "utils.fallback", module-qualified, which cbm.c
1170+
// shortens to the last dotted segment when resolving). A receiver that is itself
1171+
// a call must NOT be prefixed: `s.trim().toLowerCase()` -> "toLowerCase", since
1172+
// the receiver's text carries parens and would never resolve.
1173+
// `newExpr` resolves to its `declaredType` so `new Server {}` links to the class.
1174+
static char *extract_pkl_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
1175+
if (strcmp(nk, "newExpr") == 0) {
1176+
// `new { ... }` with an inferred type has no declaredType child.
1177+
TSNode dt = cbm_find_child_by_kind(node, "declaredType");
1178+
return ts_node_is_null(dt) ? NULL : cbm_node_text(a, dt, source);
1179+
}
1180+
1181+
bool qualified = strcmp(nk, "qualifiedAccessExpr") == 0;
1182+
if (!qualified && strcmp(nk, "unqualifiedAccessExpr") != 0) {
1183+
return NULL;
1184+
}
1185+
// No argument list -> property read, not a call.
1186+
if (ts_node_is_null(cbm_find_child_by_kind(node, "argumentList"))) {
1187+
return NULL;
1188+
}
1189+
1190+
TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver"));
1191+
TSNode name = (TSNode){0};
1192+
uint32_t nc = ts_node_named_child_count(node);
1193+
for (uint32_t i = 0; i < nc; i++) {
1194+
TSNode child = ts_node_named_child(node, i);
1195+
if (!ts_node_is_null(recv) && ts_node_eq(child, recv)) {
1196+
continue;
1197+
}
1198+
if (strcmp(ts_node_type(child), "identifier") == 0) {
1199+
name = child;
1200+
break;
1201+
}
1202+
}
1203+
if (ts_node_is_null(name)) {
1204+
return NULL;
1205+
}
1206+
char *mn = cbm_node_text(a, name, source);
1207+
if (!mn || !mn[0]) {
1208+
return NULL;
1209+
}
1210+
if (!qualified || ts_node_is_null(recv)) {
1211+
return mn;
1212+
}
1213+
if (strcmp(ts_node_type(recv), "unqualifiedAccessExpr") == 0 &&
1214+
ts_node_is_null(cbm_find_child_by_kind(recv, "argumentList"))) {
1215+
char *rt = cbm_node_text(a, recv, source);
1216+
if (rt && rt[0]) {
1217+
return cbm_arena_sprintf(a, "%s.%s", rt, mn);
1218+
}
1219+
}
1220+
return mn;
1221+
}
1222+
11641223
// Typst: a `call` node's callee is its `item` field (an ident), matching the
11651224
// def-side resolution of `#let greet(name) = ...`.
11661225
static char *extract_typst_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
@@ -1789,6 +1848,15 @@ static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, C
17891848
}
17901849
}
17911850

1851+
/* Pkl: resolve here and return unconditionally — the access-expr call node
1852+
* types double as plain property reads, so falling through to field-based or
1853+
* generic first-identifier resolution would mint a CALLS edge for every
1854+
* property read (a bare `host` has an `identifier` first child, which the
1855+
* generic fallback would happily emit). NULL here means "not a call". */
1856+
if (lang == CBM_LANG_PKL) {
1857+
return extract_pkl_callee(a, node, source, ts_node_type(node));
1858+
}
1859+
17921860
// Helm / Go templates: resolve `include "x"` / `template "x"` to the
17931861
// referenced named template so it links to the define'd Function (#338).
17941862
if (lang == CBM_LANG_GOTEMPLATE) {

‎internal/cbm/extract_usages.c‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ typedef enum {
378378
CBM_OCCURRENCE_VHDL_INTERFACE,
379379
CBM_OCCURRENCE_PINE_FUNCTION,
380380
CBM_OCCURRENCE_LLVM_FUNCTION,
381+
CBM_OCCURRENCE_PKL_DECLARATION,
381382
} CBMOccurrencePolicy;
382383

383384
typedef struct {
@@ -497,6 +498,7 @@ static const CBMOccurrenceSpec occurrence_specs[CBM_LANG_COUNT] = {
497498
[CBM_LANG_PINE] = {NULL, NULL, CBM_OCCURRENCE_PINE_FUNCTION, false},
498499
[CBM_LANG_PUPPET] = {NULL, NULL, CBM_OCCURRENCE_STANDARD, true},
499500
[CBM_LANG_LLVM_IR] = {llvm_binding_nodes, NULL, CBM_OCCURRENCE_LLVM_FUNCTION, false},
501+
[CBM_LANG_PKL] = {NULL, NULL, CBM_OCCURRENCE_PKL_DECLARATION, false},
500502
[CBM_LANG_MESON] = {NULL, meson_write_nodes, CBM_OCCURRENCE_STANDARD, true},
501503
[CBM_LANG_GN] = {NULL, gn_write_nodes, CBM_OCCURRENCE_STANDARD, true},
502504
[CBM_LANG_LINKERSCRIPT] = {NULL, linkerscript_write_nodes, CBM_OCCURRENCE_STANDARD, true},
@@ -1006,6 +1008,30 @@ static bool is_pine_function_binding(TSNode node) {
10061008
return ancestor_field_binds(node, "function_declaration_statement", fields);
10071009
}
10081010

1011+
/* Pkl declares names positionally: no production labels the declared identifier
1012+
* with a `name` field, so the generic declared-container rule never binds them
1013+
* and every method name, parameter name, and property name would be re-emitted
1014+
* as an ordinary read of itself. Each container below holds its declared name
1015+
* as named child 0; annotations, defaults, and bodies follow it and stay reads.
1016+
* Resolve against the NEAREST container so a nested declaration's own name is
1017+
* the only occurrence its parent can bind. */
1018+
static bool is_pkl_declaration_binding(TSNode node) {
1019+
static const char *const declaration_kinds[] = {"methodHeader",
1020+
"typedIdentifier",
1021+
"classProperty",
1022+
"objectProperty",
1023+
"clazz",
1024+
"typeAlias",
1025+
NULL};
1026+
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
1027+
parent = ts_node_parent(parent)) {
1028+
if (kind_in_exact_set(ts_node_type(parent), declaration_kinds)) {
1029+
return named_child_contains(parent, 0, node);
1030+
}
1031+
}
1032+
return false;
1033+
}
1034+
10091035
static bool is_policy_binding(CBMExtractCtx *ctx, TSNode node,
10101036
const CBMOccurrenceSpec *occurrence) {
10111037
switch (occurrence->policy) {
@@ -1062,6 +1088,8 @@ static bool is_policy_binding(CBMExtractCtx *ctx, TSNode node,
10621088
return is_vhdl_interface_binding(node);
10631089
case CBM_OCCURRENCE_PINE_FUNCTION:
10641090
return is_pine_function_binding(node);
1091+
case CBM_OCCURRENCE_PKL_DECLARATION:
1092+
return is_pkl_declaration_binding(node);
10651093
case CBM_OCCURRENCE_LLVM_FUNCTION:
10661094
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
10671095
parent = ts_node_parent(parent)) {

‎internal/cbm/helpers.c‎

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -651,29 +651,14 @@ int cbm_count_branching(TSNode node, const char **branching_types) {
651651

652652
// Loop node-type names across tree-sitter grammars, for loop-nesting depth.
653653
bool cbm_is_loop_node_type(const char *kind) {
654-
static const char *const loops[] = {"for_statement",
655-
"while_statement",
656-
"do_statement",
657-
"do_while_statement",
658-
"for_in_statement",
659-
"for_of_statement",
660-
"for_each_statement",
661-
"foreach_statement",
662-
"enhanced_for_statement",
663-
"for_range_loop",
664-
"c_style_for_statement",
665-
"for_expression",
666-
"while_expression",
667-
"loop_expression",
668-
"while_let_expression",
669-
"repeat_statement",
670-
"repeat_while_statement",
671-
"until",
672-
"while_modifier",
673-
"until_modifier",
674-
"for",
675-
"while",
676-
NULL};
654+
static const char *const loops[] = {
655+
"for_statement", "while_statement", "do_statement", "do_while_statement",
656+
"for_in_statement", "for_of_statement", "for_each_statement", "foreach_statement",
657+
"enhanced_for_statement", "for_range_loop", "c_style_for_statement", "for_expression",
658+
"while_expression", "loop_expression", "while_let_expression", "repeat_statement",
659+
"repeat_while_statement",
660+
// Pkl: `for (x in xs) { ... }` inside an object body.
661+
"forGenerator", "until", "while_modifier", "until_modifier", "for", "while", NULL};
677662
for (const char *const *l = loops; *l; l++) {
678663
if (strcmp(kind, *l) == 0) {
679664
return true;

‎internal/cbm/lang_specs.c‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,11 +1594,21 @@ static const char *tlaplus_branch_types[] = {"if_then_else", "case", NULL};
15941594
static const char *tlaplus_var_types[] = {"variable_declaration", NULL};
15951595
static const char *tlaplus_module_types[] = {"source_file", NULL};
15961596
static const char *pkl_func_types[] = {"classMethod", "objectMethod", NULL};
1597-
static const char *pkl_class_types[] = {"clazz", NULL};
1598-
static const char *pkl_import_types[] = {"importClause", "extendsOrAmendsClause", "extends",
1599-
"import", NULL};
1597+
static const char *pkl_class_types[] = {"clazz", "typeAlias", NULL};
1598+
static const char *pkl_import_types[] = {
1599+
"importClause", "importGlobClause", "importExpr", "extendsOrAmendsClause",
1600+
"extends", "import", NULL};
16001601
static const char *pkl_var_types[] = {"classProperty", "objectProperty", NULL};
16011602
static const char *pkl_module_types[] = {"module", NULL};
1603+
/* Both access exprs double as plain property reads; extract_pkl_callee keeps
1604+
* only the ones carrying an argumentList. `newExpr` resolves to its type. */
1605+
static const char *pkl_call_types[] = {"unqualifiedAccessExpr", "qualifiedAccessExpr", "newExpr",
1606+
NULL};
1607+
/* Control-flow only, matching every other spec (short-circuit operators are
1608+
* deliberately excluded). `forGenerator` is also a loop — see helpers.c. */
1609+
static const char *pkl_branch_types[] = {"ifExpr", "whenGenerator", "forGenerator", NULL};
1610+
static const char *pkl_throw_types[] = {"throwExpr", NULL};
1611+
static const char *pkl_decorator_types[] = {"annotation", NULL};
16021612
static const char *gomod_var_types[] = {"require_directive", "replace_directive", NULL};
16031613
static const char *gomod_import_types[] = {"require", NULL};
16041614
static const char *gomod_module_types[] = {"source_file", NULL};
@@ -2630,9 +2640,9 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
26302640

26312641
// CBM_LANG_PKL
26322642
[CBM_LANG_PKL] = {CBM_LANG_PKL, pkl_func_types, pkl_class_types, empty_types, pkl_module_types,
2633-
empty_types, pkl_import_types, empty_types, empty_types, pkl_var_types,
2634-
empty_types, empty_types, NULL, empty_types, NULL, NULL, tree_sitter_pkl,
2635-
NULL},
2643+
pkl_call_types, pkl_import_types, empty_types, pkl_branch_types,
2644+
pkl_var_types, empty_types, pkl_throw_types, NULL, pkl_decorator_types, NULL,
2645+
NULL, tree_sitter_pkl, NULL},
26362646

26372647
// CBM_LANG_GOMOD
26382648
[CBM_LANG_GOMOD] = {CBM_LANG_GOMOD, empty_types, empty_types, empty_types, gomod_module_types,

‎tests/repro/repro_call_argument_matrix_b.c‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,12 @@ static const char TLAPLUS_BOUNDED_QUANTIFICATION[] =
752752
"Guard(values) == \\A item \\in values : item = item\n"
753753
"====\n";
754754

755+
/* Pkl access expressions double as plain property reads, so the bare fixture
756+
* also proves the un-applied reference is not promoted to a call. */
757+
static const char PKL_INSIDE[] = "function accept(value: Int): Int = value\n"
758+
"function run(watched: Int): Int = accept(watched)\n";
759+
static const char PKL_BARE[] = "function run(watched: Int): Int = watched\n";
760+
755761
static const char APEX_INSIDE[] = "public class Sample {\n"
756762
" private static Integer accept(Integer value) {\n"
757763
" return value;\n"
@@ -988,6 +994,9 @@ static const RoutineArgumentCase LLVM_IR_CASE = ROUTINE_ARGUMENT_CASE(
988994
static const RoutineArgumentCase TLAPLUS_CASE = ROUTINE_ARGUMENT_CASE(
989995
"TLAPLUS", CBM_LANG_TLAPLUS, "Sample.tla", TLAPLUS_INSIDE, TLAPLUS_BARE, "bound_op", "Guard",
990996
"Accept", "values", 1, 1, 0, "TLA+ operator application with a value argument");
997+
static const RoutineArgumentCase PKL_CASE = ROUTINE_ARGUMENT_CASE(
998+
"PKL", CBM_LANG_PKL, "sample.pkl", PKL_INSIDE, PKL_BARE, "unqualifiedAccessExpr", "run",
999+
"accept", "watched", 1, 1, 0, "native Pkl method application and property-read vocabulary");
9911000
static const RoutineArgumentCase APEX_CASE = ROUTINE_ARGUMENT_CASE(
9921001
"APEX", CBM_LANG_APEX, "Sample.cls", APEX_INSIDE, APEX_BARE, "method_invocation", "run",
9931002
"accept", "watched", 1, 1, 0, "native method application");
@@ -1203,6 +1212,7 @@ DEFINE_ROUTINE_ARGUMENT_TEST(func, FUNC_CASE)
12031212
DEFINE_ROUTINE_ARGUMENT_TEST(puppet, PUPPET_CASE)
12041213
DEFINE_ROUTINE_ARGUMENT_TEST(slang, SLANG_CASE)
12051214
DEFINE_ROUTINE_ARGUMENT_TEST(llvm_ir, LLVM_IR_CASE)
1215+
DEFINE_ROUTINE_ARGUMENT_TEST(pkl, PKL_CASE)
12061216
DEFINE_ROUTINE_ARGUMENT_TEST(apex, APEX_CASE)
12071217
DEFINE_ROUTINE_ARGUMENT_TEST(pine, PINE_CASE)
12081218
DEFINE_ROUTINE_ARGUMENT_TEST(qml, QML_CASE)
@@ -1282,15 +1292,15 @@ TEST(repro_call_argument_matrix_b_domain_bitbake) {
12821292
}
12831293

12841294
enum {
1285-
ROUTINE_ARGUMENT_LANGUAGE_COUNT = 38,
1295+
ROUTINE_ARGUMENT_LANGUAGE_COUNT = 39,
12861296
MODULE_ARGUMENT_LANGUAGE_COUNT = 4,
12871297
DOMAIN_CONTROL_LANGUAGE_COUNT = 6,
12881298
MATRIX_LANGUAGE_COUNT = ROUTINE_ARGUMENT_LANGUAGE_COUNT + MODULE_ARGUMENT_LANGUAGE_COUNT +
12891299
DOMAIN_CONTROL_LANGUAGE_COUNT,
12901300
};
12911301

1292-
_Static_assert(MATRIX_LANGUAGE_COUNT == 48,
1293-
"RACKET..CHIALISP call-capable matrix must contain exactly 48 "
1302+
_Static_assert(MATRIX_LANGUAGE_COUNT == 49,
1303+
"RACKET..CHIALISP call-capable matrix must contain exactly 49 "
12941304
"language rows");
12951305

12961306
#define MATRIX_B_LANGUAGE_ROWS(X) \
@@ -1322,6 +1332,7 @@ _Static_assert(MATRIX_LANGUAGE_COUNT == 48,
13221332
X(repro_call_argument_matrix_b_routine_slang, SLANG_CASE.identity.language) \
13231333
X(repro_call_argument_matrix_b_routine_llvm_ir, LLVM_IR_CASE.identity.language) \
13241334
X(repro_call_argument_matrix_b_routine_tlaplus, TLAPLUS_CASE.identity.language) \
1335+
X(repro_call_argument_matrix_b_routine_pkl, PKL_CASE.identity.language) \
13251336
X(repro_call_argument_matrix_b_routine_apex, APEX_CASE.identity.language) \
13261337
X(repro_call_argument_matrix_b_routine_pine, PINE_CASE.identity.language) \
13271338
X(repro_call_argument_matrix_b_routine_qml, QML_CASE.identity.language) \

‎tests/repro/repro_call_node_manifest.c‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
* TLA+ bound_op is historical direct-call metadata. Bounded quantification is
1717
* not a historical ledger row; repro_call_argument_matrix_b is its separate
1818
* negative behavior guard.
19+
*
20+
* The historical-snapshot column is the audited ledger, not an archaeology
21+
* record: every registered call node owns a row, so a newly registered kind
22+
* (Pkl's access/new expressions) joins the snapshot with the change that
23+
* registers it.
1924
*/
2025
#include "test_framework.h"
2126
#include "lang_specs.h"
@@ -54,18 +59,18 @@ typedef struct {
5459
} CallNodeManifestEntry;
5560

5661
enum {
57-
EXPECTED_HISTORICAL_CALL_NODE_TOTAL = 223,
58-
EXPECTED_ACTIVE_PRIMARY_TOTAL = 193,
59-
EXPECTED_DIRECT_CALLS = 158,
60-
EXPECTED_CONSTRUCTOR_CALLS = 21,
62+
EXPECTED_HISTORICAL_CALL_NODE_TOTAL = 226,
63+
EXPECTED_ACTIVE_PRIMARY_TOTAL = 196,
64+
EXPECTED_DIRECT_CALLS = 160,
65+
EXPECTED_CONSTRUCTOR_CALLS = 22,
6166
EXPECTED_OPERATOR_CALLS = 12,
6267
EXPECTED_IMPLICIT_CALLS = 2,
6368
EXPECTED_DSL_INVOCATIONS = 14,
6469
EXPECTED_BUILD_DEPENDENCIES = 1,
6570
EXPECTED_CALLEE_WRAPPERS = 8,
6671
EXPECTED_ARGUMENT_WRAPPERS = 1,
6772
EXPECTED_CONTROL_OR_DOCUMENT_NONCALLS = 6,
68-
EXPECTED_PRIMARY_OWNERS = 193,
73+
EXPECTED_PRIMARY_OWNERS = 196,
6974
EXPECTED_SYNTHETIC_OWNERS = 10,
7075
EXPECTED_NO_CALL_EDGE_OWNERS = 20,
7176
EXPECTED_PRIMARY_OPERATOR_CALLS = 4,
@@ -297,6 +302,9 @@ static const CallNodeManifestEntry CALL_NODE_MANIFEST[] = {
297302
ENTRY(CBM_LANG_TLAPLUS, "function_evaluation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
298303
ENTRY(CBM_LANG_TLAPLUS, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
299304
ENTRY(CBM_LANG_TLAPLUS, "bound_op", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
305+
ENTRY(CBM_LANG_PKL, "unqualifiedAccessExpr", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
306+
ENTRY(CBM_LANG_PKL, "qualifiedAccessExpr", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
307+
ENTRY(CBM_LANG_PKL, "newExpr", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
300308
ENTRY(CBM_LANG_APEX, "method_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
301309
ENTRY(CBM_LANG_PINE, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
302310
ENTRY(CBM_LANG_MOJO, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),

0 commit comments

Comments
 (0)