Skip to content

Mark the range unions closed (C# 15) for compile-time exhaustiveness, on both lines #149

Open

Description

@CaffeinatedCoder

Decided 2026-10-18 (maintainer, after the #148 probe): closed, not union. Ships on both lines.

  • closed on every union type (Int32Range … TimeRange, the three NodaTime ones, YearMonthRange) and on RangeSet where a shape switch exists. closed is implicitly abstract, so the abstract modifier goes. The private constructor stays: a closed type is only assembly-scoped (CS9382 outside the assembly).
  • An in-project System.Runtime.CompilerServices.IsClosedTypeAttribute polyfill under #if !NET11_0_OR_GREATER in each shipping assembly; a net11.0 consumer reads it from metadata and gets exhaustiveness all the same (measured).
  • Consumers keep working unchanged: an existing _ => throw arm next to complete arms raises nothing; a missing arm is CS8509 naming the variant. So this is not breaking for consumers, and the ApiCompat pack should stay clean (verify).
  • Both lines. First on net11 as 11.0.0-preview.2. On 10.x it ships as a minor after .NET 11 GA, because a stable package must not build with LangVersion preview; from GA on, LangVersion 15 is stable and the 10.x line builds with the .NET 11 SDK targeting net10.0. CI on the 10 line then needs the .NET 11 SDK too (CI matrix for two lines: .NET 10 and 11 SDKs, PostgreSQL floor and 18 legs per line #152).
  • union is not adopted: it changes the representation (boxed object? value, different default) and every consumer's declared type for no gain in exhaustiveness.

The headline of 11.0.0. Today the range union is a closed class hierarchy only at runtime (AGENTS.md, "Critical Rules"): a switch over Int32Range warns CS8509 without a discard, so consumers write _ => throw, the three engines throw ShapePair.Unreachable, and EngineDispatchConventionTests parses src/**/Internals/ to make sure the discards throw. The rewrite makes the compiler carry that guarantee.

Scope, per the probe's outcome:

  • Every union type (Int32Range … TimeRange, the three NodaTime ones, YearMonthRange) and RangeSet where a shape switch exists.
  • Consumers gain compile-time exhaustiveness: a switch over a union's variants loses its _ => throw. The three engines do not: they switch over the structural interface patterns (IFiniteRange<T>, …) on IRange<T> operands, and closing the concrete classes does not make those switches exhaustive. ShapePair.Unreachable and EngineDispatchConventionTests stay as they are. Whether a per-type closed dispatch inside the engines is worth it is decided in Compile probe: C# 15 closed hierarchies and union types for the five variants, on net10.0 and net11.0 #148, not promised here.
  • JSON, TypeConverter and the EF variant retyper (RangeVariantConstantRetyper) are re-measured: an inlined constant still arrives typed as its variant.
  • ApiCompat: generate the suppression file and document every entry in the changelog's "what can break you" list; docs/migration.md gets the v10.x section showing a consumer switch before and after.
  • Native AOT: the consumer smoke test's AOT consumer must switch over a range.
  • The private constructor stays if the probe shows closed is assembly-scoped only.

Not on the 10.x line unless the probe shows the feature compiles on net10.0 with no runtime support.

Activity

  1. added this to the 11.0.0 milestone on Oct 18, 2026
  2. added
    enhancementNew feature or request
    breakingChanges an answer or a signature — needs a major
    area/rangesRange unions, variants and the shape engines
    area/range-setRangeSet and multirange algebra
    on Oct 18, 2026
  3. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Scope correction from an independent review (GPT 6 Astra, 2026-10-18), confirmed against the code: a closed hierarchy makes a switch over the concrete variants exhaustive (Int32Range.Finite, .UnboundedStart, …). The three engines in src/**/Internals/ switch over the structural interface patterns (IFiniteRange<T>, IUnboundedStartRange<T>, …) on IRange<T> operands, and closing the concrete classes does not make those switches exhaustive. So this issue's "remove the throwing discards from the engines" holds only if the engines are restructured to dispatch on closed concrete types, which they cannot do generically.

    Revised scope: compile-time exhaustiveness is for consumers (their switches over a union's variants lose the _ => throw), and ShapePair.Unreachable plus EngineDispatchConventionTests stay for the engines. Whether a per-type closed dispatch inside the engines is worth it is a question for #148's decision comment, not a promise of this issue.

    The reviewer also tested closed on the .NET 11 RC1 SDK: a switch over the variants compiled without a fallback, and removing an arm produced CS8509 naming the missing variant. Target framework of that probe unknown; #148's net10.0 question stays open.

  4. changed the title [-]Rewrite the five range variants as a C# 15 closed hierarchy or union, with compile-time exhaustiveness[/-] [+]Mark the range unions closed (C# 15) for compile-time exhaustiveness, on both lines[/+] on Oct 18, 2026
  5. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Release window, 2026-10-18 (#178, PR #180): 10.x now takes additions for six months after v11.0.0, not only until it. The 10.x half of this issue (a minor after .NET 11 GA, built with LangVersion 15 targeting net10.0) falls inside that window, so the plan above stands. It has to ship before the cut-off date CONTRIBUTING records at the v11.0.0 tag.

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

    area/range-setRangeSet and multirange algebraarea/rangesRange unions, variants and the shape enginesbreakingChanges an answer or a signature — needs a majorenhancementNew feature or requestpackage/coreAffects CodoMetis.ValueRangespackage/nodatimeAffects CodoMetis.ValueRanges.NodaTime

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions