Skip to content

C# impact: seven resolution gaps found by a 16-repo mutation oracle - #1881

Merged
swapnilpaliwal-sd merged 7 commits into
apps/integration-0.1.9from
fix/cs-impact-loop
Oct 10, 2026
Merged

swapnilpaliwal-sd merged 7 commits into
apps/integration-0.1.9from
fix/cs-impact-loop

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Symptom

On real C# libraries, impact <file:line> --tests missed most of the tests that fail when the method breaks, and selected nothing at all for a quarter of breakable methods; path <test file>.cs <method> refused every C# file endpoint.

Measured with a behavioural oracle on 16 open-source C# repositories pinned at tags at least six months old (xUnit, NUnit and MSTest; libraries, DI containers and extensions, parsers, mappers, mocking). For 50 methods per repository, sampled from the source with a Roslyn syntax walk (never from the graph), a guarded throw is inserted at entry, the repository is rebuilt and its suite run; the truth is the set of test files with a test that passed in both clean runs and fails now. 11 repositories were used for tuning; 5 were held out and scored once at the end.

Mechanism, one commit per pattern (each pattern seen in at least two repositories)

  1. A name written inside a namespace is read from it. namespace A.Tests; using Results; imports A.Results, and Reflection.Info.Create() inside namespace A names A.Reflection.Info. Both were taken as written, so the using imported nothing and client types read as library types (boundary_lib), or the receiver stayed untyped.
  2. path takes a .cs file as an endpoint. The file branch listed .ts/.tsx/.js/.mjs/.cjs/.py/.java only. It now takes every extension in ax_langs.BY_EXT; a new extension falls through to the name lookup when no file matches (Owner.cs can still name a method).
  3. A using resource is typed by its initializer. using (var w = new Writer()) / using var w = …; have declarationKind USING, which var_value_initializer did not read, so every call on the resource had an untyped receiver.
  4. The implicit base() call is an edge. A constructor without : base(..)/: this(..), or a class with no constructor at its new, runs the base's parameterless constructor. New synthesised kind implicit_base_ctor (bundle schema + ax_edges vocabulary updated).
  5. An unstaged receiver keeps its type name. var services = new ServiceCollection(); services.AddX() and Configure(s => s.AddX()) with Action<IServiceCollection> gave call_recv_type_name nothing, so a client AddX(this IServiceCollection) matched neither by name nor on the loose DI rung (csharp: a this IServiceCollection extension called on a ServiceCollection receiver is labelled a boundary call and impact omits it #1472).
  6. A static member read through its type name types the chain. Ctx.Current.Factory.Create(): the getter was an edge but its result had no type, because expr_type on a member access needed a typed qualifier and a type name has none.
  7. A constructed generic type is a static receiver. Cache<int>.Get(), Registry<A,B>.Create(): the parser emitted the generic_name receiver with no written name. It now carries the identifier (arity stays in typeArgumentCount) and the engine resolves it at that arity; every receiver without type arguments keeps arity "0", as both clauses used before.

Each commit adds a tests/cases/csharp/ case that fails on the base and passes with the fix, with controls that pass on both (a file in an unrelated namespace, a foreach variable, : base(name) / : this(..) constructors, an in-source this type, a property named like a type, a non-generic type of the same name).

Numbers (base = apps/integration-0.1.9 a811ccf)

Tuning set, 11 repos, 550 mutants (443 with a failing test), cumulative:

step micro recall macro recall precision empty selections every failing file selected path found
base 0.462 0.546 0.400 116 181 0.000
+ 1, 2 0.473 0.583 0.403 95 200 0.470
+ 3 0.549 0.607 0.382 97 210 0.497
+ 4 0.553 0.607 0.384 96 211 0.498
+ 5 0.561 0.648 0.387 83 229 0.527
+ 6 0.618 0.674 0.373 80 246 0.557
+ 7 0.624 0.682 0.374 78 249 0.572

tests verb with the break applied: recall 0.494 → 0.641. No tuning repo loses recall at any step. Precision falls with the added reach (tests that call a method without failing when it throws). Selections for mutants no test detects: 849 → 859 cumulative; step 6 alone cut them 900 → 837.

Held-out set, 5 repos, 250 mutants (199 with a failing test), scored once:

micro recall macro recall precision empty all selected path found context r@5
base 0.573 0.672 0.361 27 107 0.000 0.458
fix 0.604 0.701 0.339 21 114 0.693 0.483

One held-out repo loses one mutant's two files (0.724 → 0.712): the base selected them only through a [by name] lead at a call on a static singleton (Empty.Instance.ToText()), which step 6 now resolves correctly to the singleton's own override; the real route is lost earlier, at an out var receiver, on both.

Cost: forced full index (parse + solve), base vs fix interleaved on 10 repos: within ±1 s on 9, +2 s on one (17 → 19 s); graph.sqlite +0–2 MB; edges to client methods +0.1% to +27%.

Checks

  • tests/run.py csharp 241/241, java 333/333, typescript 269/269, javascript 307/307, python 306/306
  • graph/test/csharp/run-tests.sh (oracle self-tests, 22 engine cases, staging, remote edges) green, output identical to the base
  • tests/hook_languages.py 7/7, tests/fastpath.py --lang csharp 8/8, parser csharp-tests.ts 62/62, gen_decls_all.py --check

Still open (not in this PR)

A library calling client overrides (GetHashCode/Equals as dictionary keys, GetEnumerator through foreach/LINQ), types created by reflection (Register<TMap>(), Activator), expression-tree mappers, an Outer.Inner qualifier resolved through an enclosing type, and path's optimistic closure ignoring boundary_lib sites (a client method mislabelled as a library call reads as "independent" instead of naming the site).

swapnilpaliwal-sd and others added 7 commits October 9, 2026 14:24
`namespace Shop.Tests; using Results;` imports Shop.Results, and
`Reflection.Info.Create()` written inside namespace Shop names
Shop.Reflection.Info: C# reads the first segment of a using's name, and of a
qualified name, from the enclosing namespaces outward. The engine took both as
written, so the using imported nothing and every client type named through it
read as a library type (`new Failure(..)` was boundary_lib), and the qualified
receiver stayed untyped.

- module_in_scope: a using whose name is no namespace at all takes the child of
  the file's namespace chain that does exist (the IR does not say whether the
  using sits inside the namespace; a name that is no namespace only compiles
  there).
- type_name_cand: a type's qualified name is split at each dot into namespace
  prefix and dotted tail; the tail is a rank-1 candidate in every file whose
  namespace chain holds the prefix. Keyed from the declaration side.

Case a-namespace-name-is-read-from-its-namespace fails 4/6 on the base (the two
controls, a file in an unrelated namespace, pass on both).

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`axiomcode path Tests/OrderTests.cs Order.Place` was refused with "nothing
named 'Tests/OrderTests.cs' is declared" on every C# graph, although the file is
indexed: the file-endpoint branch matched .ts/.tsx/.js/.mjs/.cjs/.py/.java only.

Every extension a front end reads (ax_langs.BY_EXT) is now a file endpoint. An
extension outside the old list falls through to the name lookup when no indexed
file has that name, so `Owner.cs` still resolves a method `cs` of Owner (the
case's control).

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`using (var w = new Writer(..)) { w.Write(..); }` and
`using var w = new Writer();` left the receiver untyped: var_value_initializer
read LOCAL and CONST declarations only, and the IR gives these the
declarationKind USING. The resource is the disposable itself, so its
initializer is a value of its type exactly as a local's is.

Case a-using-resource-is-typed-by-its-initializer: both shapes fail on the base;
the control (a foreach variable is typed by its element, never by the
collection) passes on both.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
A constructor with no `: base(..)` / `: this(..)` runs `base()` before its body,
and a class that declares no constructor gets an implicit one that does the
same. Neither is written, so the base constructor had no caller: an abstract
base whose constructor every derived class runs read as uncalled.

New synthesised call_edges kind implicit_base_ctor (known_edge):
- from a declared constructor without an initializer (and not a primary
  constructor whose heritage passes arguments), FromExpr = the base's heritage
  type reference, as primary_ctor_base does;
- from a `new T()` site where T declares no constructor, beside its
  known_implicit_ctor row.
The target is the base's constructor that takes no argument, walking up through
bases that declare none. Registered in the bundle schema and ax_edges' kind
vocabulary (`ctor`).

Case a-constructor-runs-its-base-constructor: 4 positives fail on the base; the
controls (`: base(name)` and `: this(..)` constructors gain no implicit edge)
pass on both.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`var services = new ServiceCollection(); services.AddWidgets();` and
`Configure(s => s.AddWidgets())` with `Configure(Action<IServiceCollection> c)`
never reached the client's `AddWidgets(this IServiceCollection s)`: both
receivers' types are unstaged, so they have no group, and call_recv_type_name --
the NAME the by-name rung (3) and the loose DI rung (7, #1472) compare -- had no
clause for either.

call_recv_type_name now also gives
- an implicitly typed local initialised with `new T(..)`: the T written there;
- an implicitly typed lambda parameter: the name of the delegate argument that
  types it (lambda_param_arg_ref, the same reference param_type reads).
So the lambda's call matches by name (resolved) and the local's is a rung-7
candidate (one of a set, keeping its external label).

Case an-unstaged-receiver-keeps-its-type-name: both positives fail on the base;
controls (an extension on an in-source `this` type, a lambda typed as another
unstaged type) pass on both; extension-on-unstaged-receiver unchanged.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`var f = Context.Current.Factory; f.Create()` -- the getter of the static
property `Current` was an edge, but its result had no type: expr_type for a
MEMBER_ACCESS needed an expr_type on the qualifier, and a qualifier that names
a TYPE has none. So `.Factory` and every call after it were unresolved.

Two clauses mirror the instance ones with ref_names_type on the qualifier
(property and field). ref_names_type already refuses a name that also binds a
value, the language's member-over-type rule.

Case a-static-member-read-types-the-chain: four shapes (local, one chain, a var
of the static read, a static field) fail on the base; the "Color Color" control
-- a property named like the type is the value read -- passes on both.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`Cache<int>.Get("k")`, `Registry<A, B>.Create()` and
`Pair<int, string>.Make().Use()` were unresolved: the parser emitted a
generic_name in expression position with typeArgumentCount set and NO
potentialQualifiedName, so the receiver had no name to resolve.

- parser: a generic_name row carries its identifier as the written name (its
  arity stays in typeArgumentCount).
- engine: expr_type_argc projects the count; ref_names_type and the call
  receiver's type_ref_demand resolve the name at that arity, so `Cache<int>`
  names Cache`1 and never a non-generic Cache. Every receiver written without
  type arguments has arity "0", which is what both clauses used before.

Case a-generic-type-name-is-a-static-receiver: three positives fail on the
base; the non-generic `Cache.Get` control resolves to its own type on both.
Parser C# suite 62/62.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
@swapnilpaliwal-sd
swapnilpaliwal-sd force-pushed the fix/cs-impact-loop branch 2 times, most recently from 443a26a to c0dab0b Compare October 9, 2026 21:37
@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit 8deef3e into apps/integration-0.1.9 Oct 10, 2026
12 checks passed
@swapnilpaliwal-sd
swapnilpaliwal-sd deleted the fix/cs-impact-loop branch October 10, 2026 03:48
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