Skip to content

fix(pipeline): suppress weak Python member calls - #1903

Merged
DeusData merged 2 commits into
mainfrom
feat/1324-python-weak-member
Aug 29, 2026
Merged

fix(pipeline): suppress weak Python member calls#1903
DeusData merged 2 commits into
mainfrom
feat/1324-python-weak-member

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Fixes #1276. Distilled from #1324 with Co-authored-by: credit to @Enferlain, whose design and tests this is.

What it does

Python obj.method() calls where the receiver is neither self/cls/super() nor rooted in one of the file's imports are marked is_method, which enables the existing weak-member guard for them — suppressing suffix_match / unique_name / field_type_hint / fuzzy resolutions. Those are short-name coincidences: #1276's accelerator.print() binding to MockAccelerator.print, trainer.backward(), .step().

The cbm_tsjs_suppress_weak_method_match helper is renamed to cbm_suppress_weak_member_match with the language gate moved from the helper body to its two call sites. The body logic is byte-identical — the same drop-list, and same_module / import_map / qualified_suffix / lsp_* all still kept.

Scoped per-language, deliberately

This follows the house pattern rather than restricting cbm_registry_resolve globally: gated on file language, wired at both pass_calls.c and pass_parallel.c (wiring one produces a sequential/parallel divergence that would threaten the MT byte-identical invariant), and it keeps same_module as all three existing guards do. The lockstep requirement is now written into the code at both sites and into the helper's contract comment, so a language cannot be added to one resolver only.

ArkTS is retained. Main's gate had grown CBM_LANG_ARKTS after 0df990aa; the original PR predates that, and taking its line verbatim would have silently dropped ArkTS from the weak-member guard, reintroducing the #592/#606 false-edge class. Both gates now read PYTHON || JAVASCRIPT || TYPESCRIPT || TSX || ARKTS.

python/S6 now records a trade this repo already accepted for TypeScript

The guard costs one real edge class: an inherited method called through a parameter-held receiver. test_lsp_resolution_probe.c's python/S6 case is rewritten to assert calls == 0 exactly, with the same anti-vacuous tripwire TypeScript uses.

That is not a new concession — it is the same decision, measured. TS's S6 was rewritten from >= 1 to == 0 when the receiver-aware guard landed, and its comment explains why: the pre-guard pass came from "a unique_name registry fallback" where "a weak short-name guess happened to be right" in a 2-file fixture. Python was measured to be in precisely that state — strategy=unique_name, cands=1, conf=0.7500 — and that measurement is now in the probe comment so the next reader inherits it rather than re-deriving it.

The flip-back condition is recorded alongside: >= 1 once py_lsp_cross resolves inheritance, mirroring what TS's probe already says. That work is underway separately.

Verification

  • Build clean under -Werror, asserted before trusting any run.
  • lsp_resolution_probe 87 passed, 0 failed — same total as main; the S6 case executed by name.
  • registry extraction pipeline complexity 639 passed, 0 failed.
  • Revert-check in both directions: disabling only the Python gate at both call sites gives FAIL test_lsp_resolution_probe.c:975: calls == 1, expected 0; restoring returns 87 passed. The rewritten assertion passes at 0 and fails at 1, so it binds rather than merely accepting whatever happens.
  • Wrong-suite guard: the three genuinely-new tests appear 0× in a main-based binary's output and 1× here.
  • Aliased imports still exempt (import tools as toolkittoolkit.format()), pinned by test — #1371 changed Python aliased-from-import handling since the original PR, and CBMImport.local_name survives it.

Notes

python_receiver_is_exempt linear-scans the file's imports per Python attribute call. That is O(file), not O(corpus) — imports.count is tens — so it cannot trip the complexity guard, and the bound is documented in the helper rather than optimised into allocation and lifetime complexity on the extraction hot path.

Whole-file clang-format is not a usable local signal here: pristine main reports thousands of violations under Homebrew LLVM 22.1.8. Changed ranges only: 0 violations across all 10 files. CI's lint-ci remains the authority.

DeusData and others added 2 commits August 29, 2026 11:44
Python attribute calls were not classified as member calls, so weak
short-name strategies (suffix_match / unique_name / field_type_hint)
could bind `obj.method()` to an arbitrary same-named project symbol —
`accelerator.print()` resolving to MockAccelerator.print.

Flag a Python attribute call is_method when its receiver is unresolved,
and extend the existing TS/JS weak-member guard to Python. Receivers that
ARE known stay exempt: self/cls, super(), and any identifier bound by one
of the file's imports (including the root of an attribute chain), so
Python's canonical `module.function()` cross-file call still resolves.

The guard helper is renamed cbm_tsjs_suppress_weak_method_match ->
cbm_suppress_weak_member_match. Its drop-list is unchanged, and the
per-language gate stays at the two call sites, which enumerate the same
language set in pass_calls.c and pass_parallel.c so the sequential and
parallel resolvers cannot diverge. ArkTS remains in that set.

python/S6/inherited_method in the resolution probe now asserts calls == 0,
recording the same trade TypeScript took when the receiver-aware guard
landed: the edge it used to produce came from a unique_name registry
fallback (measured on main: strategy=unique_name, cands=1, conf=0.7500)
where a weak short-name guess happened to be right in a 2-file fixture.
It flips back to >= 1 once py_lsp_cross resolves inheritance, matching the
flip-back note already carried on the TypeScript S6 case.

Fixes #1276
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: Enferlain <15861396+Enferlain@users.noreply.github.com>
parallel_lsp_index_exact_ambiguity_does_not_fall_through_to_legacy
asserted alpha_edges == 1, which the Python weak-member guard suppresses.

That assertion is a side effect, not the test's subject. Its subject —
that an ambiguous exact LSP match must not fall through to the
lower-ranked legacy semantic row — is unchanged: beta_edges == 0,
legacy_edges == 0, and all four probe assertions (ambiguity.injected,
legacy_injected, carrier_allows_fallback, shared_matcher_failed_closed)
still hold. Verified before changing the number, not after.

The harness deliberately injects an ambiguous exact match and forces the
shared matcher to fail closed, so value.render() drops past the LSP into
the registry with three same-named candidates (Alpha/Beta/Legacy.render)
and bound by suffix_match -- measured: strategy=suffix_match, cands=3,
conf=0.5500. That is a 1-in-3 guess that happened to land on Alpha:
exactly the weak member-call class this change suppresses, and the same
accepted trade recorded on python/S6 in test_lsp_resolution_probe.c.

Not over-suppression: sibling tests in this suite index the same
value.render() source and resolve it via lsp_method (cands=1,
conf=0.9000), a strategy the guard keeps -- they still assert 1. Only the
sabotaged-LSP path degrades to a weak textual guess. Flips back to 1 once
py_lsp_cross resolves the receiver on this path.

Refs #1276
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
注册 for free to join this conversation on GitHub. Already have an account? 登录 to comment

标签

None yet

项目

None yet

Development

Successfully merging this pull request may close these issues.

Python unresolved member calls produce false-positive CALLS edges via suffix_match and field_type_hint

1 participant