Skip to content

Commit ae0a192

Browse files
committed
fix(crossrepo): capture std::/boost:: callees for cross-lib linking (baselib_callees); OFF byte-identical
extract_callees previously dropped every std::/boost:: callee via SYSTEM_PREFIXES at parse time, so collect_transitive_deps never saw them and the cross-repo base-lib linker was DEAD for boost/std (the two highest-value libs). Capture those callees in a separate FunctionDef.baselib_callees list (same parse walk), consulted by the cross-lib linker only when --global-symbol-index is given. Normal callees/call-edges and the flag-OFF enriched doc are byte-identical to before (verified vs HEAD). Bounds unchanged: depth-1, cap 12/doc, 6144 tok/doc, tagged crosslib:<repo>. Update process_commits.py for the new 2-tuple return. Add real-libclang-parse regression tests (the unit tests hand-built callees and bypassed the SYSTEM_PREFIXES filter, which is why the bug was hidden).
1 parent ea5d3c2 commit ae0a192

3 files changed

Lines changed: 199 additions & 30 deletions

File tree

tests/test_crossrepo_global_symbols.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,108 @@ def test_crosslink_on_pulls_tagged_chunk(tmp_path):
148148
f"expected root->crosslib call_edge; edges={edges} root={root_idx} cl={cl_idx}"
149149
)
150150
reader.close()
151+
152+
153+
# --------------------------------------------------------------------------- #
154+
# REAL libclang parse-path regression. The unit tests above hand-build
155+
# FunctionDef.callees with a 'boost::' qname, which bypasses extract_callees and
156+
# its SYSTEM_PREFIXES filter. In the LIVE pipeline, base-lib callees go through
157+
# extract_callees first -- and 'std::'/'boost::' are in SYSTEM_PREFIXES, so they
158+
# were silently dropped from `callees` and never reached the cross-lib linker
159+
# (the feature was dead for boost/std). This test parses real source with
160+
# libclang to prove the cross-linkable base-lib callee survives into
161+
# FunctionDef.baselib_callees and is pulled when the index is enabled.
162+
# --------------------------------------------------------------------------- #
163+
def _parse_one(ip, src_text, tmp_path):
164+
ip._configure_libclang()
165+
from clang.cindex import Index # type: ignore
166+
src = tmp_path / "u.cpp"
167+
src.write_text(src_text)
168+
idx = Index.create()
169+
funcs, _types = ip.parse_translation_unit(
170+
str(src), idx, ["-std=c++17"], str(tmp_path))
171+
return funcs
172+
173+
174+
def test_extract_callees_splits_baselib_from_normal():
175+
ip = _load_index_project()
176+
import clang.cindex # noqa: F401 (skip cleanly if libclang missing)
177+
out = ip.extract_callees # signature check: returns a 2-tuple
178+
assert ip.is_crosslinkable_baselib("boost::beast::make_printable")
179+
assert ip.is_crosslinkable_baselib("std::sort")
180+
assert not ip.is_crosslinkable_baselib("myrepo::helper")
181+
assert not ip.is_crosslinkable_baselib("memcpy")
182+
assert callable(out)
183+
184+
185+
def test_real_parse_baselib_callee_survives_and_pulls(tmp_path):
186+
ip = _load_index_project()
187+
try:
188+
import clang.cindex # noqa: F401
189+
except Exception as exc: # pragma: no cover
190+
pytest.skip(f"libclang unavailable: {exc}")
191+
192+
# Root calls a boost:: symbol (filtered out of `callees`) + a local helper.
193+
src = (
194+
"namespace boost { namespace algorithm { template<class R> "
195+
"void trim(R& r); } }\n"
196+
"static int local_helper(int x) { return x + 1; }\n"
197+
"int do_work(int s) {\n"
198+
" boost::algorithm::trim(s);\n"
199+
" return local_helper(s);\n"
200+
"}\n"
201+
)
202+
funcs = _parse_one(ip, src, tmp_path)
203+
root = next(f for f in funcs if f.name == "do_work")
204+
205+
# REGRESSION CORE: boost:: callee is NOT in `callees` (SYSTEM_PREFIXES) but
206+
# IS captured in baselib_callees so the cross-lib linker can see it.
207+
assert not any(c.startswith("boost::") for c in root.callees), root.callees
208+
assert "boost::algorithm::trim" in root.baselib_callees, root.baselib_callees
209+
# IPC round-trip preserves the new field.
210+
assert ip.FunctionDef.from_dict(root.to_dict()).baselib_callees == \
211+
root.baselib_callees
212+
213+
# Now build a store with the boost def and confirm an actual PULL.
214+
db, body = _make_store(tmp_path)
215+
reader = ip.GlobalSymbolReader(db)
216+
idx = ip.ProjectIndex()
217+
for f in funcs:
218+
idx.add_function(f)
219+
docs = ip.build_training_documents(
220+
idx, max_tokens=16384, enriched=True, global_symbols=reader)
221+
root_doc = [d for d in docs if "do_work" in d["text"]][0]
222+
cl = [b for b in root_doc["chunk_boundaries"]
223+
if (b.get("dep_source") or "").startswith("crosslib:")]
224+
assert len(cl) == 1, cl
225+
assert cl[0]["dep_source"] == "crosslib:boost"
226+
assert cl[0]["name"] == "trim"
227+
assert "trim impl" in root_doc["text"]
228+
reader.close()
229+
230+
231+
def test_real_parse_off_has_no_crosslink(tmp_path):
232+
"""With global_symbols=None the base-lib callee is captured but never pulled
233+
(OFF behavior unchanged: no crosslib chunk, no provenance)."""
234+
ip = _load_index_project()
235+
try:
236+
import clang.cindex # noqa: F401
237+
except Exception as exc: # pragma: no cover
238+
pytest.skip(f"libclang unavailable: {exc}")
239+
src = (
240+
"namespace boost { namespace algorithm { template<class R> "
241+
"void trim(R& r); } }\n"
242+
"int do_work(int seed_value_for_token_floor) {\n"
243+
" int accumulator = seed_value_for_token_floor + 1;\n"
244+
" boost::algorithm::trim(accumulator);\n"
245+
" return accumulator + seed_value_for_token_floor;\n"
246+
"}\n"
247+
)
248+
funcs = _parse_one(ip, src, tmp_path)
249+
idx = ip.ProjectIndex()
250+
for f in funcs:
251+
idx.add_function(f)
252+
docs = ip.build_training_documents(idx, max_tokens=16384, enriched=True)
253+
root_doc = [d for d in docs if "do_work" in d["text"]][0]
254+
assert not any(b.get("dep_source") for b in root_doc["chunk_boundaries"])
255+
assert "trim impl" not in root_doc["text"]

tools/clang_indexer/index_project.py

Lines changed: 92 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,10 @@ def classify_build_file(fname: str) -> str | None:
348348
return None
349349

350350

351-
# System/stdlib function prefixes (skip for dependency tracking)
351+
# System/stdlib function prefixes (skip for dependency tracking). UNCHANGED from
352+
# the original: callees under these prefixes are NOT recorded as normal callees,
353+
# so the local dep-resolution / call-edge path behaves EXACTLY as before whether
354+
# or not cross-repo linking is enabled. (OFF behavior is byte-identical.)
352355
SYSTEM_PREFIXES = (
353356
'std::', 'boost::', '__builtin', '__', 'operator', 'printf', 'fprintf',
354357
'sprintf', 'snprintf', 'scanf', 'malloc', 'calloc', 'realloc', 'free',
@@ -357,19 +360,30 @@ def classify_build_file(fname: str) -> str | None:
357360
'assert', 'pthread_', 'EXPECT_', 'ASSERT_', 'TEST',
358361
)
359362

363+
# Base-library NAMESPACE prefixes that ARE cross-linkable (present in the global
364+
# symbol store built by scripts/crossrepo/build_global_symbol_index.py). A callee
365+
# under one of these is filtered OUT of the normal `callees` list (above) but is
366+
# captured SEPARATELY in FunctionDef.baselib_callees so the cross-repo linker can
367+
# resolve it WITHOUT changing the normal callee/call-edge behavior. We do NOT
368+
# include libc-style bare names here (memcpy/strlen/...) because those are not in
369+
# the A1 selection and would only add noise; only the namespaced base libs.
370+
CROSSLINKABLE_NS_PREFIXES = ('std::', 'boost::')
371+
360372

361373
class FunctionDef:
362374
"""A function definition with its source location and call references."""
363375
__slots__ = ['name', 'qualified_name', 'file', 'line', 'end_line', 'text',
364376
'callees', 'referenced_types', 'dep_level', 'is_definition',
365-
'ast_depth', 'sibling_index', 'ast_node_type']
377+
'ast_depth', 'sibling_index', 'ast_node_type',
378+
'baselib_callees']
366379

367380
def __init__(self, name: str, qualified_name: str, file: str, line: int,
368381
text: str, callees: list, is_definition: bool = True,
369382
end_line: int = 0, ast_depth: list[int] | None = None,
370383
sibling_index: list[int] | None = None,
371384
ast_node_type: list[int] | None = None,
372-
referenced_types: list | None = None):
385+
referenced_types: list | None = None,
386+
baselib_callees: list | None = None):
373387
self.name = name
374388
self.qualified_name = qualified_name
375389
self.file = file
@@ -382,6 +396,10 @@ def __init__(self, name: str, qualified_name: str, file: str, line: int,
382396
# SAME libclang parse as callees so it round-trips IPC and feeds the
383397
# offline type_refs/type_edges builders. Mirrors `callees`.
384398
self.referenced_types = list(referenced_types or [])
399+
# Cross-linkable base-lib callees (std::/boost::) dropped from `callees`
400+
# by the normal system-prefix filter, kept SEPARATELY for the optional
401+
# cross-repo linker. Empty/ignored unless --global-symbol-index is given.
402+
self.baselib_callees = list(baselib_callees or [])
385403
self.dep_level = 0
386404
self.is_definition = is_definition
387405
self.ast_depth = list(ast_depth or [])
@@ -399,6 +417,7 @@ def to_dict(self) -> dict:
399417
'sibling_index': self.sibling_index,
400418
'ast_node_type': self.ast_node_type,
401419
'referenced_types': self.referenced_types,
420+
'baselib_callees': self.baselib_callees,
402421
}
403422

404423
@classmethod
@@ -537,6 +556,19 @@ def is_system_function(name: str | None) -> bool:
537556
return any(name.startswith(p) for p in SYSTEM_PREFIXES)
538557

539558

559+
def is_crosslinkable_baselib(name: str | None) -> bool:
560+
"""True if ``name`` is a cross-linkable base-lib symbol (std::/boost::).
561+
562+
Such a callee is filtered out of the normal ``callees`` list (it matches
563+
SYSTEM_PREFIXES) but is captured separately so the cross-repo linker can
564+
resolve it against the global symbol index. This does NOT affect normal
565+
callee/call-edge behavior.
566+
"""
567+
if not isinstance(name, str) or not name:
568+
return False
569+
return any(name.startswith(p) for p in CROSSLINKABLE_NS_PREFIXES)
570+
571+
540572
_DROP_CLANG_ARG_PREFIXES = (
541573
"-fsanitize=",
542574
"-fno-sanitize=",
@@ -813,23 +845,39 @@ def _cursor_text_and_metadata(
813845
)
814846

815847

816-
def extract_callees(cursor: Cursor) -> list[str]:
817-
"""Extract all function call references from a cursor's children."""
818-
callees = set()
848+
def extract_callees(cursor: Cursor) -> tuple[list[str], list[str]]:
849+
"""Extract function call references from a cursor's children.
850+
851+
Returns ``(callees, baselib_callees)``:
852+
853+
* ``callees`` — normal resolvable callees (UNCHANGED filter: system/stdlib
854+
prefixes dropped). Feeds local dep-resolution + call-edges exactly as
855+
before, so OFF behavior is byte-identical.
856+
* ``baselib_callees`` — callees that were dropped by the normal filter BUT
857+
are cross-linkable base-lib namespaces (std::/boost::). Captured here in the
858+
SAME walk so the cross-repo linker can resolve them against the global
859+
symbol index WITHOUT touching the normal callee path. Ignored entirely when
860+
cross-repo linking is disabled.
861+
"""
862+
callees: set[str] = set()
863+
baselib_callees: set[str] = set()
819864

820865
def walk(node: Cursor):
821866
if node.kind == CursorKind.CALL_EXPR:
822867
ref = node.referenced
823868
if ref and ref.spelling:
824869
# Get fully qualified name
825870
qname = get_qualified_name(ref)
826-
if qname and not is_system_function(qname):
827-
callees.add(qname)
871+
if qname:
872+
if not is_system_function(qname):
873+
callees.add(qname)
874+
elif is_crosslinkable_baselib(qname):
875+
baselib_callees.add(qname)
828876
for child in node.get_children():
829877
walk(child)
830878

831879
walk(cursor)
832-
return list(callees)
880+
return list(callees), list(baselib_callees)
833881

834882

835883
_REFERENCED_TYPE_DECL_KINDS = frozenset({
@@ -1020,7 +1068,7 @@ def visit(cursor):
10201068
)
10211069
)
10221070
if text and len(text) >= 20:
1023-
callees = extract_callees(cursor)
1071+
callees, baselib_callees = extract_callees(cursor)
10241072
referenced_types = extract_referenced_types(cursor)
10251073
qname = get_qualified_name(cursor)
10261074
functions.append(FunctionDef(
@@ -1036,6 +1084,7 @@ def visit(cursor):
10361084
sibling_index=func_sibling_index,
10371085
ast_node_type=func_ast_node_type,
10381086
referenced_types=referenced_types,
1087+
baselib_callees=baselib_callees,
10391088
))
10401089
return
10411090

@@ -1380,6 +1429,29 @@ def collect_transitive_deps(
13801429
the base lib (depth-1 only), and we SKIP trivial/tiny inline bodies — that is
13811430
bounded by ``crosslink_budget`` + the builder's body-len floor.
13821431
"""
1432+
crosslink_active = (
1433+
global_symbols is not None
1434+
and crosslink_visited is not None
1435+
and crosslink_budget is not None
1436+
)
1437+
1438+
def _try_crosslink(callee: str) -> None:
1439+
# Depth-1 only: a pulled base-lib def is a LEAF; we never enqueue it, so
1440+
# its base-lib-internal callees are not followed. Bounds (count + token
1441+
# budget) are enforced by crosslink_budget. ONE clear path; a miss (not a
1442+
# selected base-lib symbol) is simply a no-op, never a fallback.
1443+
if callee in crosslink_visited:
1444+
return
1445+
if not crosslink_budget.has_room():
1446+
return
1447+
hit = global_symbols.lookup(callee)
1448+
if hit is None:
1449+
return
1450+
if not crosslink_budget.can_afford(hit["token_est"]):
1451+
return
1452+
crosslink_visited.add(callee)
1453+
crosslink_budget.spend(hit["token_est"])
1454+
13831455
visited = {root_qname}
13841456
queue = deque([(root_qname, 0)])
13851457
deps = []
@@ -1398,25 +1470,16 @@ def collect_transitive_deps(
13981470
visited.add(callee)
13991471
deps.append(callee)
14001472
queue.append((callee, depth + 1))
1401-
elif (
1402-
global_symbols is not None
1403-
and crosslink_visited is not None
1404-
and crosslink_budget is not None
1405-
):
1406-
# Unresolved callee: try the cross-repo base-lib index. Depth-1
1407-
# only — a pulled base-lib def is a LEAF here; we never enqueue it
1408-
# so its base-lib-internal callees are not followed.
1409-
if callee in crosslink_visited:
1410-
continue
1411-
if not crosslink_budget.has_room():
1412-
continue
1413-
hit = global_symbols.lookup(callee)
1414-
if hit is None:
1415-
continue
1416-
if not crosslink_budget.can_afford(hit["token_est"]):
1417-
continue
1418-
crosslink_visited.add(callee)
1419-
crosslink_budget.spend(hit["token_est"])
1473+
elif crosslink_active:
1474+
# An unresolved callee (not local) MIGHT be a base-lib symbol
1475+
# (e.g. a vendored/forward-declared libc-style name). Try it.
1476+
_try_crosslink(callee)
1477+
# Base-lib-namespaced callees (std::/boost::) were filtered out of
1478+
# `callees` by the normal system-prefix filter and live here. They are
1479+
# the PRIMARY cross-link source. Only consulted when cross-linking is on.
1480+
if crosslink_active:
1481+
for callee in func.baselib_callees:
1482+
_try_crosslink(callee)
14201483

14211484
return deps
14221485

tools/clang_indexer/process_commits.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ def visit(cursor):
718718
)
719719
)
720720
if text and len(text) >= 20:
721-
callees = extract_callees(cursor)
721+
callees, baselib_callees = extract_callees(cursor)
722722
referenced_types = extract_referenced_types(cursor)
723723
qname = get_qualified_name(cursor)
724724
start_line = cursor.extent.start.line
@@ -736,6 +736,7 @@ def visit(cursor):
736736
sibling_index=func_sibling_index,
737737
ast_node_type=func_ast_node_type,
738738
referenced_types=referenced_types,
739+
baselib_callees=baselib_callees,
739740
))
740741

741742
elif cursor.kind in (CursorKind.CLASS_DECL, CursorKind.STRUCT_DECL,

0 commit comments

Comments
 (0)