Skip to content
Open
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
68 changes: 68 additions & 0 deletions internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,65 @@ static char *extract_nickel_callee(CBMArena *a, TSNode node, const char *source,
return NULL;
}

// Pkl: `unqualifiedAccessExpr` / `qualifiedAccessExpr` are the same node whether
// they are a call (`helper(a)`) or a bare property read (`host`) — the only
// discriminator is an `argumentList` child, so both are gated on it. For a
// qualified call the method name is the `identifier` child that is not the
// `receiver`; the receiver is prefixed only when it is itself a plain name
// (`utils.fallback(a)` -> "utils.fallback", module-qualified, which cbm.c
// shortens to the last dotted segment when resolving). A receiver that is itself
// a call must NOT be prefixed: `s.trim().toLowerCase()` -> "toLowerCase", since
// the receiver's text carries parens and would never resolve.
// `newExpr` resolves to its `declaredType` so `new Server {}` links to the class.
static char *extract_pkl_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
if (strcmp(nk, "newExpr") == 0) {
// `new { ... }` with an inferred type has no declaredType child.
TSNode dt = cbm_find_child_by_kind(node, "declaredType");
return ts_node_is_null(dt) ? NULL : cbm_node_text(a, dt, source);
}

bool qualified = strcmp(nk, "qualifiedAccessExpr") == 0;
if (!qualified && strcmp(nk, "unqualifiedAccessExpr") != 0) {
return NULL;
}
// No argument list -> property read, not a call.
if (ts_node_is_null(cbm_find_child_by_kind(node, "argumentList"))) {
return NULL;
}

TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver"));
TSNode name = (TSNode){0};
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_named_child(node, i);
if (!ts_node_is_null(recv) && ts_node_eq(child, recv)) {
continue;
}
if (strcmp(ts_node_type(child), "identifier") == 0) {
name = child;
break;
}
}
if (ts_node_is_null(name)) {
return NULL;
}
char *mn = cbm_node_text(a, name, source);
if (!mn || !mn[0]) {
return NULL;
}
if (!qualified || ts_node_is_null(recv)) {
return mn;
}
if (strcmp(ts_node_type(recv), "unqualifiedAccessExpr") == 0 &&
ts_node_is_null(cbm_find_child_by_kind(recv, "argumentList"))) {
char *rt = cbm_node_text(a, recv, source);
if (rt && rt[0]) {
return cbm_arena_sprintf(a, "%s.%s", rt, mn);
}
}
return mn;
}

// Typst: a `call` node's callee is its `item` field (an ident), matching the
// def-side resolution of `#let greet(name) = ...`.
static char *extract_typst_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
Expand Down Expand Up @@ -1638,6 +1697,15 @@ static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, C
}
}

/* Pkl: resolve here and return unconditionally — the access-expr call node
* types double as plain property reads, so falling through to field-based or
* generic first-identifier resolution would mint a CALLS edge for every
* property read (a bare `host` has an `identifier` first child, which the
* generic fallback would happily emit). NULL here means "not a call". */
if (lang == CBM_LANG_PKL) {
return extract_pkl_callee(a, node, source, ts_node_type(node));
}

// Helm / Go templates: resolve `include "x"` / `template "x"` to the
// referenced named template so it links to the define'd Function (#338).
if (lang == CBM_LANG_GOTEMPLATE) {
Expand Down
28 changes: 28 additions & 0 deletions internal/cbm/extract_usages.c
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ typedef enum {
CBM_OCCURRENCE_VHDL_INTERFACE,
CBM_OCCURRENCE_PINE_FUNCTION,
CBM_OCCURRENCE_LLVM_FUNCTION,
CBM_OCCURRENCE_PKL_DECLARATION,
} CBMOccurrencePolicy;

typedef struct {
Expand Down Expand Up @@ -522,6 +523,7 @@ static const CBMOccurrenceSpec occurrence_specs[CBM_LANG_COUNT] = {
[CBM_LANG_PINE] = {NULL, NULL, CBM_OCCURRENCE_PINE_FUNCTION, false},
[CBM_LANG_PUPPET] = {NULL, NULL, CBM_OCCURRENCE_STANDARD, true},
[CBM_LANG_LLVM_IR] = {llvm_binding_nodes, NULL, CBM_OCCURRENCE_LLVM_FUNCTION, false},
[CBM_LANG_PKL] = {NULL, NULL, CBM_OCCURRENCE_PKL_DECLARATION, false},
[CBM_LANG_MESON] = {NULL, meson_write_nodes, CBM_OCCURRENCE_STANDARD, true},
[CBM_LANG_GN] = {NULL, gn_write_nodes, CBM_OCCURRENCE_STANDARD, true},
[CBM_LANG_LINKERSCRIPT] = {NULL, linkerscript_write_nodes, CBM_OCCURRENCE_STANDARD, true},
Expand Down Expand Up @@ -1015,6 +1017,30 @@ static bool is_pine_function_binding(TSNode node) {
return ancestor_field_binds(node, "function_declaration_statement", fields);
}

/* Pkl declares names positionally: no production labels the declared identifier
* with a `name` field, so the generic declared-container rule never binds them
* and every method name, parameter name, and property name would be re-emitted
* as an ordinary read of itself. Each container below holds its declared name
* as named child 0; annotations, defaults, and bodies follow it and stay reads.
* Resolve against the NEAREST container so a nested declaration's own name is
* the only occurrence its parent can bind. */
static bool is_pkl_declaration_binding(TSNode node) {
static const char *const declaration_kinds[] = {"methodHeader",
"typedIdentifier",
"classProperty",
"objectProperty",
"clazz",
"typeAlias",
NULL};
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
if (kind_in_exact_set(ts_node_type(parent), declaration_kinds)) {
return named_child_contains(parent, 0, node);
}
}
return false;
}

static bool is_policy_binding(CBMExtractCtx *ctx, TSNode node,
const CBMOccurrenceSpec *occurrence) {
switch (occurrence->policy) {
Expand Down Expand Up @@ -1071,6 +1097,8 @@ static bool is_policy_binding(CBMExtractCtx *ctx, TSNode node,
return is_vhdl_interface_binding(node);
case CBM_OCCURRENCE_PINE_FUNCTION:
return is_pine_function_binding(node);
case CBM_OCCURRENCE_PKL_DECLARATION:
return is_pkl_declaration_binding(node);
case CBM_OCCURRENCE_LLVM_FUNCTION:
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
Expand Down
31 changes: 8 additions & 23 deletions internal/cbm/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -536,29 +536,14 @@ int cbm_count_branching(TSNode node, const char **branching_types) {

// Loop node-type names across tree-sitter grammars, for loop-nesting depth.
bool cbm_is_loop_node_type(const char *kind) {
static const char *const loops[] = {"for_statement",
"while_statement",
"do_statement",
"do_while_statement",
"for_in_statement",
"for_of_statement",
"for_each_statement",
"foreach_statement",
"enhanced_for_statement",
"for_range_loop",
"c_style_for_statement",
"for_expression",
"while_expression",
"loop_expression",
"while_let_expression",
"repeat_statement",
"repeat_while_statement",
"until",
"while_modifier",
"until_modifier",
"for",
"while",
NULL};
static const char *const loops[] = {
"for_statement", "while_statement", "do_statement", "do_while_statement",
"for_in_statement", "for_of_statement", "for_each_statement", "foreach_statement",
"enhanced_for_statement", "for_range_loop", "c_style_for_statement", "for_expression",
"while_expression", "loop_expression", "while_let_expression", "repeat_statement",
"repeat_while_statement",
// Pkl: `for (x in xs) { ... }` inside an object body.
"forGenerator", "until", "while_modifier", "until_modifier", "for", "while", NULL};
for (const char *const *l = loops; *l; l++) {
if (strcmp(kind, *l) == 0) {
return true;
Expand Down
22 changes: 16 additions & 6 deletions internal/cbm/lang_specs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1546,11 +1546,21 @@ static const char *tlaplus_branch_types[] = {"if_then_else", "case", NULL};
static const char *tlaplus_var_types[] = {"variable_declaration", NULL};
static const char *tlaplus_module_types[] = {"source_file", NULL};
static const char *pkl_func_types[] = {"classMethod", "objectMethod", NULL};
static const char *pkl_class_types[] = {"clazz", NULL};
static const char *pkl_import_types[] = {"importClause", "extendsOrAmendsClause", "extends",
"import", NULL};
static const char *pkl_class_types[] = {"clazz", "typeAlias", NULL};
static const char *pkl_import_types[] = {
"importClause", "importGlobClause", "importExpr", "extendsOrAmendsClause",
"extends", "import", NULL};
static const char *pkl_var_types[] = {"classProperty", "objectProperty", NULL};
static const char *pkl_module_types[] = {"module", NULL};
/* Both access exprs double as plain property reads; extract_pkl_callee keeps
* only the ones carrying an argumentList. `newExpr` resolves to its type. */
static const char *pkl_call_types[] = {"unqualifiedAccessExpr", "qualifiedAccessExpr", "newExpr",
NULL};
/* Control-flow only, matching every other spec (short-circuit operators are
* deliberately excluded). `forGenerator` is also a loop — see helpers.c. */
static const char *pkl_branch_types[] = {"ifExpr", "whenGenerator", "forGenerator", NULL};
static const char *pkl_throw_types[] = {"throwExpr", NULL};
static const char *pkl_decorator_types[] = {"annotation", NULL};
static const char *gomod_var_types[] = {"require_directive", "replace_directive", NULL};
static const char *gomod_import_types[] = {"require", NULL};
static const char *gomod_module_types[] = {"source_file", NULL};
Expand Down Expand Up @@ -2553,9 +2563,9 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {

// CBM_LANG_PKL
[CBM_LANG_PKL] = {CBM_LANG_PKL, pkl_func_types, pkl_class_types, empty_types, pkl_module_types,
empty_types, pkl_import_types, empty_types, empty_types, pkl_var_types,
empty_types, empty_types, NULL, empty_types, NULL, NULL, tree_sitter_pkl,
NULL},
pkl_call_types, pkl_import_types, empty_types, pkl_branch_types,
pkl_var_types, empty_types, pkl_throw_types, NULL, pkl_decorator_types, NULL,
NULL, tree_sitter_pkl, NULL},

// CBM_LANG_GOMOD
[CBM_LANG_GOMOD] = {CBM_LANG_GOMOD, empty_types, empty_types, empty_types, gomod_module_types,
Expand Down
16 changes: 14 additions & 2 deletions scripts/run-test-wave.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
SKIPPED = re.compile(r"(?:^|, )(?P<skipped>[0-9]+) skipped")
SLOW_SUITES = frozenset(("incremental", "store_arch", "daemon_runtime"))
POLL_SECONDS = 0.05
# Floor for how long an external Windows helper (taskkill.exe, powershell.exe)
# may take to answer. This is deliberately NOT --kill-grace: that flag budgets
# how long a doomed process may take to die, while this budgets process spawn
# plus CIM startup on a loaded runner, which routinely exceeds a second. Wiring
# the two together made a small kill grace flake the whole wave -- taskkill
# timing out is reported as "could not prove cleanup", and the descendant probe
# that runs afterwards fails closed on its own timeout.
WINDOWS_HELPER_TIMEOUT_SECONDS = 30


@dataclass
Expand Down Expand Up @@ -123,6 +131,10 @@ def start_suite(
)


def windows_helper_timeout(kill_grace: int) -> int:
return max(kill_grace, WINDOWS_HELPER_TIMEOUT_SECONDS)


def windows_descendants(pid: int, timeout: int) -> bool:
"""True if any live process still claims `pid` as its parent.

Expand Down Expand Up @@ -167,7 +179,7 @@ def terminate_process_tree(active: ActiveSuite, kill_grace: int) -> None:
# how a deliberately-hanging fixture suite reddened a release run.
# taskkill /T cannot walk a tree from a dead PID, so prove cleanup
# the only way still available -- nothing is parented to it.
if windows_descendants(process.pid, kill_grace):
if windows_descendants(process.pid, windows_helper_timeout(kill_grace)):
raise RuntimeError(
f"suite {active.name!r} leader exited leaving live descendants"
)
Expand All @@ -185,7 +197,7 @@ def terminate_process_tree(active: ActiveSuite, kill_grace: int) -> None:
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=kill_grace,
timeout=windows_helper_timeout(kill_grace),
)
except (OSError, subprocess.TimeoutExpired):
completed = None
Expand Down
17 changes: 14 additions & 3 deletions tests/repro/repro_call_argument_matrix_b.c
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,12 @@ static const char TLAPLUS_BOUNDED_QUANTIFICATION[] =
"Guard(values) == \\A item \\in values : item = item\n"
"====\n";

/* Pkl access expressions double as plain property reads, so the bare fixture
* also proves the un-applied reference is not promoted to a call. */
static const char PKL_INSIDE[] = "function accept(value: Int): Int = value\n"
"function run(watched: Int): Int = accept(watched)\n";
static const char PKL_BARE[] = "function run(watched: Int): Int = watched\n";

static const char APEX_INSIDE[] = "public class Sample {\n"
" private static Integer accept(Integer value) {\n"
" return value;\n"
Expand Down Expand Up @@ -958,6 +964,9 @@ static const RoutineArgumentCase LLVM_IR_CASE = ROUTINE_ARGUMENT_CASE(
static const RoutineArgumentCase TLAPLUS_CASE = ROUTINE_ARGUMENT_CASE(
"TLAPLUS", CBM_LANG_TLAPLUS, "Sample.tla", TLAPLUS_INSIDE, TLAPLUS_BARE, "bound_op", "Guard",
"Accept", "values", 1, 1, 0, "TLA+ operator application with a value argument");
static const RoutineArgumentCase PKL_CASE = ROUTINE_ARGUMENT_CASE(
"PKL", CBM_LANG_PKL, "sample.pkl", PKL_INSIDE, PKL_BARE, "unqualifiedAccessExpr", "run",
"accept", "watched", 1, 1, 0, "native Pkl method application and property-read vocabulary");
static const RoutineArgumentCase APEX_CASE = ROUTINE_ARGUMENT_CASE(
"APEX", CBM_LANG_APEX, "Sample.cls", APEX_INSIDE, APEX_BARE, "method_invocation", "run",
"accept", "watched", 1, 1, 0, "native method application");
Expand Down Expand Up @@ -1166,6 +1175,7 @@ DEFINE_ROUTINE_ARGUMENT_TEST(func, FUNC_CASE)
DEFINE_ROUTINE_ARGUMENT_TEST(puppet, PUPPET_CASE)
DEFINE_ROUTINE_ARGUMENT_TEST(slang, SLANG_CASE)
DEFINE_ROUTINE_ARGUMENT_TEST(llvm_ir, LLVM_IR_CASE)
DEFINE_ROUTINE_ARGUMENT_TEST(pkl, PKL_CASE)
DEFINE_ROUTINE_ARGUMENT_TEST(apex, APEX_CASE)
DEFINE_ROUTINE_ARGUMENT_TEST(pine, PINE_CASE)
DEFINE_ROUTINE_ARGUMENT_TEST(qml, QML_CASE)
Expand Down Expand Up @@ -1243,15 +1253,15 @@ TEST(repro_call_argument_matrix_b_domain_bitbake) {
}

enum {
ROUTINE_ARGUMENT_LANGUAGE_COUNT = 36,
ROUTINE_ARGUMENT_LANGUAGE_COUNT = 37,
MODULE_ARGUMENT_LANGUAGE_COUNT = 4,
DOMAIN_CONTROL_LANGUAGE_COUNT = 6,
MATRIX_LANGUAGE_COUNT = ROUTINE_ARGUMENT_LANGUAGE_COUNT + MODULE_ARGUMENT_LANGUAGE_COUNT +
DOMAIN_CONTROL_LANGUAGE_COUNT,
};

_Static_assert(MATRIX_LANGUAGE_COUNT == 46,
"RACKET..OBJECTSCRIPT_ROUTINE call-capable matrix must contain exactly 46 "
_Static_assert(MATRIX_LANGUAGE_COUNT == 47,
"RACKET..OBJECTSCRIPT_ROUTINE call-capable matrix must contain exactly 47 "
"language rows");

#define MATRIX_B_LANGUAGE_ROWS(X) \
Expand Down Expand Up @@ -1283,6 +1293,7 @@ _Static_assert(MATRIX_LANGUAGE_COUNT == 46,
X(repro_call_argument_matrix_b_routine_slang, SLANG_CASE.identity.language) \
X(repro_call_argument_matrix_b_routine_llvm_ir, LLVM_IR_CASE.identity.language) \
X(repro_call_argument_matrix_b_routine_tlaplus, TLAPLUS_CASE.identity.language) \
X(repro_call_argument_matrix_b_routine_pkl, PKL_CASE.identity.language) \
X(repro_call_argument_matrix_b_routine_apex, APEX_CASE.identity.language) \
X(repro_call_argument_matrix_b_routine_pine, PINE_CASE.identity.language) \
X(repro_call_argument_matrix_b_routine_qml, QML_CASE.identity.language) \
Expand Down
18 changes: 13 additions & 5 deletions tests/repro/repro_call_node_manifest.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
* TLA+ bound_op is historical direct-call metadata. Bounded quantification is
* not a historical ledger row; repro_call_argument_matrix_b is its separate
* negative behavior guard.
*
* The historical-snapshot column is the audited ledger, not an archaeology
* record: every registered call node owns a row, so a newly registered kind
* (Pkl's access/new expressions) joins the snapshot with the change that
* registers it.
*/
#include "test_framework.h"
#include "lang_specs.h"
Expand Down Expand Up @@ -54,18 +59,18 @@ typedef struct {
} CallNodeManifestEntry;

enum {
EXPECTED_HISTORICAL_CALL_NODE_TOTAL = 219,
EXPECTED_ACTIVE_PRIMARY_TOTAL = 189,
EXPECTED_DIRECT_CALLS = 155,
EXPECTED_CONSTRUCTOR_CALLS = 20,
EXPECTED_HISTORICAL_CALL_NODE_TOTAL = 222,
EXPECTED_ACTIVE_PRIMARY_TOTAL = 192,
EXPECTED_DIRECT_CALLS = 157,
EXPECTED_CONSTRUCTOR_CALLS = 21,
EXPECTED_OPERATOR_CALLS = 12,
EXPECTED_IMPLICIT_CALLS = 2,
EXPECTED_DSL_INVOCATIONS = 14,
EXPECTED_BUILD_DEPENDENCIES = 1,
EXPECTED_CALLEE_WRAPPERS = 8,
EXPECTED_ARGUMENT_WRAPPERS = 1,
EXPECTED_CONTROL_OR_DOCUMENT_NONCALLS = 6,
EXPECTED_PRIMARY_OWNERS = 189,
EXPECTED_PRIMARY_OWNERS = 192,
EXPECTED_SYNTHETIC_OWNERS = 10,
EXPECTED_NO_CALL_EDGE_OWNERS = 20,
EXPECTED_PRIMARY_OPERATOR_CALLS = 4,
Expand Down Expand Up @@ -296,6 +301,9 @@ static const CallNodeManifestEntry CALL_NODE_MANIFEST[] = {
ENTRY(CBM_LANG_TLAPLUS, "function_evaluation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TLAPLUS, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_TLAPLUS, "bound_op", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PKL, "unqualifiedAccessExpr", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PKL, "qualifiedAccessExpr", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PKL, "newExpr", CONSTRUCTOR_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_APEX, "method_invocation", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_PINE, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
ENTRY(CBM_LANG_MOJO, "call", DIRECT_CALL, PRIMARY_EXTRACTOR, true, true),
Expand Down
Loading
Loading