Skip to content

Receipt same-module call-target support as a Method Query Source capability - #9938

Merged
richlander merged 2 commits into
mainfrom
feature/same-module-call-targets
Oct 12, 2026
Merged

richlander merged 2 commits into
mainfrom
feature/same-module-call-targets

Conversation

@richlander

@richlander richlander commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

Refs #8700. This is prerequisite 2 of 6 for the unsafe member census on Assembly Analysis Operation (#5254, Unsafe member findings).

#8701 and #8704 implemented #8700 on LibraryCallGraphAnalysisResult, which belongs to the Library Body Analysis infrastructure being deleted. This PR moves the judgment into one owner, SameModuleMethodResolver, so an Assembly Analysis producer can use it without a second resolver.

SameModuleMethodResolver gets candidates from two sources:

  • Library Body Analysis: MethodDefinitionMap, the existing eager declared-method map, now a subclass of the resolver. ResolveTarget delegates to it unchanged.
  • Method Query Source: the new CallTargets layer (SameModuleCallTargetLookup). It is declared, execution-scoped, built lazily, shared across request-set lanes, and receipted. It does not imply ModuleLookup.

Normative owner: docs/design/method-query-source.md#same-module-call-target-support. The target semantics stay as Direct-call target resolution defines them.

How the lookup stays bounded

  • calli: returns unresolved (indirect) and reads no metadata.
  • Operand that peels to a same-image MethodDef (through a MethodSpec, or a MemberRef on a MethodDef parent): binds by token. It reads the declaring type, the signature-depth guard, and the signature header. It decodes no signature.
  • Any other member reference: decoded in the caller's generic scope. Binding by signature uses a TypeDef index, built once, keyed by the same TypeRef identity the map uses. It decodes only the same-named methods of the matched type, once per type and name.
  • MethodSpec instantiation: not decoded, since it does not affect the target.
  • Unreadable metadata: throws and fails the visited body, as Library Body Analysis does.

The receipt (SameModuleCallTargetCoverage) reports:

  • TypeDef rows read;
  • MethodDef rows read;
  • the full signature-blob length of each enumerated step;
  • query count and querying methods.

It attributes work to the physical group and to each request the same way #9831's support does.

Evidence

Parity: MethodQuerySource_CallTargetsMatchLibraryBodyAnalysis checks every physical call site against LibraryCallGraphAnalysisResult.ResolveTarget, keyed by body, IL offset, and operand token, in both directions. It runs over:

  • the callee-resolution fixture;
  • the Analysis test assembly;
  • System.Collections.Immutable;
  • System.Linq.

A one-off run also matched on System.Private.CoreLib and System.Text.Json; those two are left out of the suite to keep it fast.

Synthetic cases: each SameModuleTarget_* case now also asserts source parity (unmatched, ambiguous, unsupported, malformed, single-definition).

Receipt and boundary gates:

  • SameModuleTarget_UnsupportedTokenBoundCalleeReceiptsItsProbes: an independent hand count of the caller row, the definition row, and the guard bytes when a call fails its site checks early.
  • CallTargetsAbsentWhenUndeclared
  • CallTargetsRequireDeclaration
  • CallTargetsReceiptExactMethodWork: exact work equals a fresh oracle lookup, and the TypeDef index is read once.
  • CallTargetsBindDefinitionTokensWithoutTypeIndex: no index, and the receipt reports only guard bytes.
  • CallTargetsSharedOnceAcrossLanes

Suites (Release):

Suite Passed Skipped
ILInspector.Analysis.Tests 2,291 8
ILInspector.Research.Tests 414 0

Performance

NativeAOT linux-x64 on perf-2 under perf-guard (CPU 99.80% idle), 2026-10-10T23:33:19Z–23:43:01Z, 25 interleaved samples per side after 3 warm-ups. Base is 2768de627 (main) and head is 6240dfb22. Binary size: 147,593,856 → 147,581,248 bytes (−12,608).

Both sections route through the declared-method map whose resolution logic moved into SameModuleMethodResolver.

Section Assembly Base median / p95 Head median / p95 Δ median Rows Output
Unsafe Members System.Runtime.Numerics 145.3 / 147.6 ms 145.7 / 148.1 ms +0.4 ms (+0.3%) 239 identical
Unsafe Members System.Text.Json 294.2 / 302.4 ms 294.7 / 310.4 ms +0.5 ms (+0.2%) 314 identical
Unsafe Members System.Private.CoreLib 2383.0 / 2436.4 ms 2370.3 / 2420.8 ms −12.7 ms (−0.5%) 14,128 identical
Top Leverage System.Runtime.Numerics 190.2 / 193.7 ms 190.0 / 193.4 ms −0.2 ms (−0.1%) 864 identical
Top Leverage System.Text.Json 601.1 / 615.4 ms 602.9 / 609.7 ms +1.8 ms (+0.3%) 4,395 identical
Top Leverage System.Private.CoreLib 6815.8 / 6937.2 ms 6788.7 / 6889.2 ms −27.1 ms (−0.4%) 42,706 identical

Every difference is within noise. No production producer declares CallTargets yet, so these numbers cover the Library Body Analysis path through the shared resolver. The lookup's own cost will be measured when the census adopts it.

Review

  • Round 1: one low finding (F1, an uncounted definition-probe row), fixed in candidate 2.
  • Round 2: CLEAN at 6240dfb22.

The round-2 observations carry forward to the census slice, the first consumer:

  • an aggregate work bound or memoization per (MemberRef, caller scope);
  • additional malformed-metadata differences;
  • CallerScope cache-field ordering.

Non-claims

🤖 Generated with Claude Code

…bility

Move typed same-module callee resolution (#8700) into one owner,
SameModuleMethodResolver, with two candidate sources: Library Body
Analysis's declared-method map and a lazy, receipted Method Query Source
lookup over metadata. The new CallTargets layer lets an Assembly Analysis
producer resolve a call operand to a current-module MethodDef, an external
declaring-type origin, a runtime-provided array member, or a typed
unresolved reason without the transitional module lookup.

Token-bound calls decode no signature; by-signature binding indexes the
module's TypeDefs once and decodes only same-named candidates of the matched
type. Parity with LibraryCallGraphAnalysisResult.ResolveTarget is gated on
every physical call site of the callee-resolution fixture, the Analysis test
assembly, System.Collections.Immutable, System.Linq, and the synthetic
malformed-metadata cases.

Refs #8700

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@richlander

Copy link
Copy Markdown
Owner Author

Round 1 reconciliation (head d1f1c78d7)

Reviewer: Claude Opus. Result: not clean, 1 blocking finding (low severity).

F1 (accepted): definition-probe MethodDef row under-counted. On a token-bound query that fails the call-site checks early (an operand signature past the depth guard, or a caller with an invalid generic declaration), Resolve reads the definition's MethodDef row, but only HasInvalidGenericDeclaration counts it, and that path is skipped. Resolution in the next candidate (implementation): count the definition probe once, where Resolve reads the row, and add a gate with an independent count for an unsupported token-bound callee.

Non-blocking observations, all taken in the next candidate (design text):

  1. Matching failure behaviour on malformed metadata. Remove "as it does in Library Body Analysis" and list the known differences on hand-authored metadata: MethodSpec instantiation validity, and DeclaredMethods excluding MethodDefs whose identity could not be built. The header-only generic-declaration probe will also treat unreadable headers as an invalid declaration, as Library Body Analysis does, so that difference goes away.
  2. Gate adequacy: the parity reverse count was confirmed sound. F1's gate adds an independent row count.
  3. Document the remembered caller-scope probe alongside the index and candidate caches.
  4. The token-bound bullet should name the operand's signature-depth guard.

🤖 Generated with Claude Code

A token-bound query reads the definition's MethodDef row for its declaring
type even when the call site fails its checks first, so count that probe
where it is read rather than in the generic-declaration check. An unreadable
generic-declaration header is an invalid declaration, as Library Body
Analysis judges it. The design text now lists the remaining
hand-authored-metadata differences and the remembered caller scope.

Refs #8700

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@richlander

Copy link
Copy Markdown
Owner Author

Round 2 reconciliation (head 6240dfb22)

Reviewer: Claude Opus. Result: CLEAN. The reviewer ran the *CallTargets* and *SameModuleTarget* gates in Release at this head: 15 passed.

Prior items:

Item Disposition
F1 Resolved. The definition probe is counted where it is read, and the new gate asserts an independent count.
Observations 1, 3 and 4 Resolved.

Non-blocking observations, carried forward. No push for optional feedback.

  1. Aggregate work bound. Each query decodes its MemberRef and TypeSpec parent again, with no cache and no assembly budget. Library Body Analysis decodes each MemberRef once under a 4M-char structural budget. Each decode is individually bounded, but crafted input can make total work grow with call sites × signature width. No production consumer declares the layer yet. The first consumer, the census producer, will either memoize per (MemberRef, caller scope) under a stated aggregate bound or state the bound explicitly. That is a design obligation of the census slice.

  2. More hand-authored-metadata differences:

    • the Library Body Analysis decode-budget failure;
    • MethodSpec target kind or unsupported target;
    • a TypeDef name that cannot be read, which fails every by-signature query in the module;
    • unreceipted TypeSpecs nested inside a signature.

    All outcomes stay contained. They are carried to the same slice, which will extend the list or close the gaps.

  3. CallerScope cache-field ordering. If GetTypeFromDefinition throws, the cache fields are left in a mixed state. Sequential visits make this practically unreachable. The fix, assigning the fields after computing them, will land with the consumer slice.

🤖 Generated with Claude Code

@richlander richlander added the review-clean PR has met the adversarial review standard per AGENTS.md. label Oct 10, 2026
@richlander
richlander merged commit ec29fa1 into main Oct 12, 2026
15 checks passed
@richlander
richlander deleted the feature/same-module-call-targets branch October 12, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-clean PR has met the adversarial review standard per AGENTS.md.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant