Skip to content

fix(registry): break suffix_match ties by kind, C# stub, then QN order, not depth - #2578

Merged
DeusData merged 1 commit into
mainfrom
fix/suffix-tiebreak-ref-stubs
Oct 10, 2026
Merged

DeusData merged 1 commit into
mainfrom
fix/suffix-tiebreak-ref-stubs

Conversation

@DeusData

Copy link
Copy Markdown
Owner

What

When several same-named definitions tie for a suffix_match call, the winner is now chosen in this order:

  1. The candidate a call can actually target. Function/Method wins over any other definition, then over an unknown label, then over Variable/Field.
  2. For C# only, the implementation over a .NET reference-assembly stub (src/libraries/<Asm>/ref/<Asm>.cs, throw null bodies).
  3. The lexicographically smaller QN.

This replaces "least nested QN, then smaller QN" (1a34255). That change made ties deterministic, and determinism stays: the rule is still a pure function of the candidate set. But nesting depth says nothing about which definition a call means.

Why

An audit of main against v0.11.0 (2026-10-10) found three resolution regressions. Each was traced by bisect to that one tie rule:

corpus what went wrong v0.11.0 main
django Model.objects.X(...) → QuerySet.X. Library.filter, EngineHandler.all and HttpResponseBase.get are each one segment shallower, so they won. 57 % 24 %
Kotlin (Exposed) CALLS edges on Variable/Field targets. where(...) went to the property var where. 785 1,648
C# (dotnet/runtime) CALLS edges on ref/ stubs. The stubs are shallower, and ref sorts before src. 4.9 % 14.1 %

Measured

macOS arm64, one cold full index per corpus, current main cb8b641 vs this PR:

metric main this PR
django objects.X → QuerySet 23.7 % 41.4 %
django CALLS → Variable 1,022 741
Kotlin CALLS → Variable 1,648 480
Java (elasticsearch) CALLS → Variable/Field 62,313 42,957
C# CALLS → ref/ stubs 143,799 52,000
C# CALLS → Variable/Field 33,779 28,662
TypeScript CALLS → Variable 7,978 7,341

The rest of the django gap needs Manager → QuerySet typing, which no tie order can supply.

The kernel cases behind 1a34255 keep their answers: dev_name is the only Function among the struct fields, and include/linux sorts before tools/virtio. Two kernel indexes with this PR are identical: 8,504,091 nodes, 1,844,907 CALLS, and the same CALLS-set hash.

Tests

  • registry_tie_prefers_callable_over_property
  • registry_tie_ignores_nesting_depth
  • registry_tie_prefers_csharp_implementation_over_ref_stub (it also guards that the stub rule is C#-only)

All three are RED on main and RED again with the registry change reverted.

Local CI on this exact tree:

platform passed failed skipped
macOS 9,115 0 11
Linux arm64 container 8,953 0 10
Windows arm64 VM 8,943 0 90

The 8 Windows guard checks are green. make lint-ci is clean.

…r, not depth

1a34255 made suffix_match ties deterministic by preferring the least
nested QN, then the smaller QN. Determinism was the point, and it stays.
But depth says nothing about which same-named definition a call means.
An audit of main against v0.11.0 (2026-10-10) traced three resolution
regressions to that one rule:

- django: about 5,600 `Model.objects.X(...)` calls tie between
  `QuerySet.X` and same-named methods elsewhere. `Library.filter`,
  `EngineHandler.all` and `HttpResponseBase.get` are each one segment
  shallower, so the QuerySet share fell from 57 % to 24 %.
- Kotlin / Java: a call went to a property or field that merely shares
  the callee's name. Kotlin `where(...)` went to `var where`, and Java
  `esr.indexMode()` to a field whose QN lacks its class segment.
  Kotlin CALLS edges on Variable/Field targets went 785 -> 1,648.
- C#: .NET reference assemblies (`src/libraries/<Asm>/ref/<Asm>.cs`,
  `throw null` bodies) are shallower than the implementation, and "ref"
  sorts before "src". 14.1 % of dotnet/runtime's CALLS landed on such
  stubs (4.9 % in v0.11.0).

A tie now goes to:
1. the more plainly callable candidate: Function/Method, then any other
   definition, then an unknown label, then Variable/Field;
2. for C# only, an implementation over a reference-assembly stub;
3. the lexicographically smaller QN.

This is still a pure function of the candidate set (O9). The kernel
cases behind 1a34255 keep their answers: `dev_name` is the only Function
among the fields, and include/linux sorts before tools/virtio.

Measured on the bench corpora (macOS arm64, main cb8b641 vs this
change, one cold full index each):
- django: `objects.X` -> QuerySet 23.7 % -> 41.4 %; CALLS -> Variable
  1,022 -> 741
- Kotlin (Exposed): CALLS -> Variable 1,648 -> 480
- Java (elasticsearch): CALLS -> Variable/Field 62,313 -> 42,957
- C# (dotnet/runtime): CALLS -> ref/ stubs 143,799 -> 52,000;
  CALLS -> Variable/Field 33,779 -> 28,662
- TypeScript: CALLS -> Variable 7,978 -> 7,341

The rest of the django gap needs Manager -> QuerySet typing, which no
tie order can supply.

Tests:
- registry_tie_prefers_callable_over_property
- registry_tie_ignores_nesting_depth
- registry_tie_prefers_csharp_implementation_over_ref_stub

All three are RED on main and RED again with the registry change
reverted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData merged commit ec4fbd3 into main Oct 10, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant