Skip to content

fix(cs-lsp): resolve nested types through type qualifiers, bind global-namespace types - #2867

Merged
DeusData merged 4 commits into
mainfrom
fix/cs-lsp-nested-types-global-ns
Nov 1, 2026
Merged

DeusData merged 4 commits into
mainfrom
fix/cs-lsp-nested-types-global-ns

Conversation

@DeusData

@DeusData DeusData commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Distilled from #2401 by @ersintarhan, with thanks. Commit 1 carries their change
(credited with Co-authored-by); commits 2-4 add a recursion bound, C# spec lookup
order for the global namespace, and a twin exclusion.

What changes

  • A qualified name A.B whose qualifier is a TYPE now resolves as a nested type
    (new Command.Result(...), new Grain.Handler(dep).Handle()). Previously the
    short-name fallback picked by registration order.
  • Top-level types declared without a namespace bind as global-namespace types.
  • The nested-type qualifier walk is depth-bounded (8). A self or mutual alias
    such as using Loop = Loop.Inner; used to recurse until the stack overflowed.
  • Lookup follows the C# spec: enclosing namespaces innermost-out, then usings
    inside namespace bodies, then the global namespace, then compilation-unit
    usings.
  • A type counts as global only if its file parsed cleanly. The vendored grammar
    does not parse a ref partial struct with members, which dissolved the
    namespaces of reference assemblies; read as global, those types outranked the
    real imported ones.
  • Global-namespace types are never pooled as partial twins (an unrelated
    same-name type in another project no longer outranks the base classes of
    the receiver).

Test plan

  • cs_lsp: cslsp_cyclic_alias_qualifier_terminates (RED without the bound: ASan
    stack-overflow), cslsp_global_namespace_types_are_not_partial_twins (RED:
    binds the unrelated twin), cslsp_decl_namespace_global_needs_clean_parse.
  • parallel: parallel_csharp_nested_type_and_global_namespace_binding
    (compilation-unit vs namespace-body using probes; RED with the previous
    order), parallel_csharp_parse_damaged_namespace_is_not_global (RED without
    the clean-parse rule, sequential and parallel).
  • Local macOS: cs_lsp 64/64, parallel 89/89, pipeline 438/438, complexity 9/9,
    lsp_resolution_probe 88/88, c_lsp seal_cs_shared_registry_readonly.
  • clang-format clean on touched sources, cppcheck 2.20 clean, memory-core
    ratchet passes.

CALLS census

dotnet/runtime 60108ba6, src/libraries, same machine, fresh caches, main c07cb8e vs this branch:

  • Nodes 539,556 -> 539,556; C# CALLS 657,400 -> 657,434 (+34).
  • LSP-resolved 255,391 -> 255,430; textual fallback 402,009 -> 402,004.
  • 54 call sites retargeted, spot-checked to the right nested type, e.g.
    Bmi1.X64.TrailingZeroCount (was BigInteger.TrailingZeroCount),
    AdvSimd.Arm64.UnzipEven (was Sve.UnzipEven),
    HeaderStringValues.Enumerator.MoveNext (was Activity.Enumerator.MoveNext).
  • 34 new edges, e.g. through using ExceptionUtil = ...ExceptionUtility.
  • 14 calls into a genuine global type moved from textual to LSP resolution.
  • Cost of spec order: 146 edges keep their target but move from LSP to
    textual fallback, because a global Helpers in one test project now
    outranks a compilation-unit using elsewhere. No wrong retargets observed.
  • Index wall time (info only): 59.4 s -> 60.9 s.

Refs #2401.

DeusData and others added 4 commits November 1, 2026 01:07
…l-ns types

Follow-up to #2347 (issue #2120). Step 8b binds same-name TOP-LEVEL types
by declared namespace, but two shapes still fell through to the short-name
fallback, where the prefix score against a path-derived QN is 0 for every
variant and registration order decided:

1. A qualified A.B whose qualifier A is a TYPE, not a namespace:
   new Command.Result(...) and new Grain.Handler(dep).Handle(...).
   cs_visible_namespaces only tried A as a namespace, and nested types
   carry no namespace_qn, so the scan never had a candidate. Resolution
   now falls back to resolving the qualifier as a type (recursively, one
   segment shorter) and binding the nested member through the registry.

2. A top-level type with no namespace declaration (global namespace):
   cs_top_level_namespace returned NULL for it, making it invisible to
   step 8b. Top-level C# types now register namespace_qn "" and
   cs_visible_namespaces appends the global namespace last.

Test: parallel_csharp_nested_type_and_global_namespace_binding covers a
nested-type constructor and the member call on its result, a chained
constructor receiver, and a same-name type split across the global
namespace and an imported namespace, in the sequential pipeline and in
the parallel pipeline with the production Tier-2 C# registry. Decoys are
registered first. Adapted to current main: the two constructor-edge
checks gate the parallel path only, because the sequential constructor
edge is still drawn by the textual constructor carrier, a layer this
change does not touch.

Distilled from #2401: the net change of commits d11be54 and 613060e.
The temporary debug prints added in d11be54 and the stale copy of #2347
the branch carried (already on main as 50d19f8) are not taken.

Co-authored-by: Ersin Tarhan <[email protected]>
Signed-off-by: Martin Vogel <[email protected]>
Resolving a qualified type name A.B that no namespace binds now resolves
the qualifier A as a type first (nested types). A using alias whose target
is qualified by the alias itself re-enters that walk through the alias
step without end: `using Loop = Loop.Inner;` resolves Loop to the target
Loop.Inner, whose qualifier is Loop again, and a mutual pair (`using
Ping = Pong.Inner; using Pong = Ping.Inner;`) cycles the same way. Both
are legal C# when namespaces of those names exist, and the walk overflowed
the stack on them.

The walk now carries a depth counter in the per-file context and stops
after CS_QUALIFIER_MAX_DEPTH (8) levels. One level is one type-nesting
step: a namespace-qualified top-level type binds without recursing, so
real nesting stays far below the bound.

Test: cs_lsp::cslsp_cyclic_alias_qualifier_terminates (self and mutual
alias cycles; the file must still resolve Svc.Go). RED without the bound:
AddressSanitizer stack-overflow, exit 134. GREEN with it.

Signed-off-by: Martin Vogel <[email protected]>
C# looks a simple type name up namespace by namespace, innermost outward.
At each namespace N it checks the members of N, then the usings of the
namespace declaration enclosing the reference. The global namespace is
the last N, and its declaration is the compilation unit. So a type
declared without a namespace hides a type that only a compilation-unit
`using` imports, while a `using` inside a namespace body still wins over
the global namespace. Step 8b used to rank every using before the global
namespace.

- CBMCSUsing records whether a directive sits inside a namespace body
  (in_namespace). cs_collect_imports tracks it per node, including the
  compilation-unit siblings after a file-scoped namespace; the cross-file
  entry pre-loads the pipeline import list without placement and the AST
  walk upgrades it.
- cs_visible_namespaces ranks a simple name as: enclosing namespaces
  innermost outward, namespace-body usings, the global namespace "", and
  last the compilation-unit usings.

Ranking the global namespace that high also requires that only a type
proven global claims it. The vendored grammar does not parse a
`ref partial struct` with members, and error recovery dissolves the
enclosing namespace, so every type after it in that namespace surfaced
with no namespace. Mapped to "" those types outranked the real, imported
declaration: on dotnet/runtime 5,313 CALLS moved from implementation
files into reference assemblies (System.Runtime.cs, which declares
DefaultInterpolatedStringHandler as a `ref partial struct`).
cs_type_decl_namespace now returns "" only when the compilation unit
parsed without errors and NULL (unknown, invisible to step 8b, as on
main) otherwise; cs_top_level_namespace passes that through.

Tests:
- parallel_csharp_nested_type_and_global_namespace_binding: the
  compilation-unit `using` probe now expects the global type (spec), and
  a new namespace-body `using` probe expects the imported one. RED with
  the previous order: cu_using=0/0.
- parallel_csharp_parse_damaged_namespace_is_not_global (sequential and
  parallel). RED without the clean-parse rule: real=0 no_damaged=0 on
  both paths.
- cs_lsp::cslsp_decl_namespace_global_needs_clean_parse: extractor
  contract ("" for a clean global type, the namespace when found, never
  "" after a parse error).

Signed-off-by: Martin Vogel <[email protected]>
cs_lookup_method searches the other declarations of a type (same simple
name, same declared namespace: `partial` pieces, reference-assembly
copies) before it walks the base classes. With global-namespace types now
carrying the namespace "", every same-name type declared without a
namespace anywhere in the repository became a twin of every other one.
Across a repository that is a different type per assembly (each test
project has its own Program or Helper), so a member missing on the
receiver bound to an unrelated type ahead of the receiver's own base.

cs_namespace_twins returns no twins for a type in the global namespace.

Test: cs_lsp::cslsp_global_namespace_types_are_not_partial_twins runs the
production Tier-2 C# registry builder over cross-file defs: two unrelated
global Helper types and a base class. RED without the guard:
stop_base=0 stop_unrelated_global_twin=1. GREEN with it.

Signed-off-by: Martin Vogel <[email protected]>
@DeusData
DeusData merged commit 6e0c59f into main Nov 1, 2026
28 checks passed
@DeusData
DeusData deleted the fix/cs-lsp-nested-types-global-ns branch November 1, 2026 01:51
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