Skip to content

A bare CLR-only annotation resolves but emits an unqualified name: SPY0908 CS0246 (the non-generic half of #1765) #1830

Description

@antonsynd

Measured on branch v5/clr-resolution (P4 remediation R5) and present at 311252e33.

def take(sb: StringBuilder) -> str:
    return sb.to_string()

def main() -> None:
    print("ok")
error[SPY0908]: internal error: generated C# failed to compile
  (CS0246: The type or namespace name 'StringBuilder' could not be found ...)

Emitted:

public static string Take(StringBuilder sb)

Class

BuiltinRegistry.TryResolveClrType resolves a bare type name through its namespace list with no import at all, so the annotation type-checks. The emitter then names it from the symbol: TypeSyntaxMapper.GetMappedTypeNameFromSymbol routes a CLR-backed symbol through the qualification authority only when ClrType is { IsGenericType: true }, so a non-generic CLR-fallback type falls through to the short-name path and binds only if a using in the prelude happens to cover its namespace. List[int] looked fine for the same reason (using System.Collections.Generic); System.Text is not in the prelude, so StringBuilder is a hard SPY0908.

This is the non-generic half of #1765's "every CLR-backed type is named from its reflected type" contract. #1765's generic half is fixed on v5/clr-resolution (the registry entries for IEnumerable/IEnumerator now name the open generic definitions and the !typeSymbol.IsGeneric carve-out is gone), which is what made this half visible as a matrix hole.

Cells

ClrTypeResolutionPriorityTests (new, src/Sharpy.Compiler.Tests/Semantic/) covers kind {Sharpy-only, collision, CLR-only} x spelling {bare, imported, module-qualified, aliased, backtick}. The ClrOnly x bare cell is the only one carried as a [Fact(Skip = ...)] citing this issue; imported / module-qualified / aliased all run. Deleting the Skip is this issue's acceptance.

Either widen the qualification gate to every non-Sharpy ClrType (the seam is GetMappedTypeNameFromSymbol, one condition), or refuse a bare CLR name with no import by name instead of resolving it. Whichever is chosen, the fix is one predicate, not a per-namespace using list.

Activity

  1. antonsynd commented on Sep 12, 2026

    @antonsynd
    OwnerAuthor

    Ruling (owner, 2026-09-11, R-AQ): qualify. Bare CLR-name resolution through the fallback namespace list is established (Math, DateTime, DayOfWeek bare are passing controls), so the emitter follows the checker: GetMappedTypeNameFromSymbol routes EVERY non-Sharpy ClrType through the qualification authority, generic or not. One predicate, no per-namespace using list. Acceptance: the ClrOnly × bare Skip in ClrTypeResolutionPriorityTests is deleted.

    Class: one qualification authority — every emitted CLR-backed or registry-backed name comes from the reflected type, on every spelling {bare, imported, module-qualified, aliased, backtick} × kind {Sharpy-only (#1831), collision, CLR-only} × {generic, non-generic}. #1863 (alias mangled to GUID) is the alias-dereference point of the same authority.

    Placement: P4b.

  2. antonsynd commented on Sep 16, 2026

    @antonsynd
    OwnerAuthor

    Fixed in P4b (mainline @ 8b596d9). One qualification predicate emits every CLR-backed type name from its reflected System.Type (37f47d5). Verified: def take(sb: StringBuilder) emits global::System.Text.StringBuilder; ClrOnly × bare un-skipped and green with the emit assertion. No unqualified CLR name remains.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions