Skip to content

fix(postconditions)!: Copy and compare arrays and the value types of System.Collections.Frozen - #63

Merged
cwouyang merged 21 commits into
masterfrom
fix/old-copies-frozen-arrays
Oct 11, 2026
Merged

cwouyang merged 21 commits into
masterfrom
fix/old-copies-frozen-arrays

Conversation

@cwouyang

Copy link
Copy Markdown
Owner

Old<T>() shared every type whose namespace is System.Collections.Frozen. The runtime reports
the namespace of an array type as that of its element type, so Old<T>() returned the original
FrozenSet<T>[] and, with it, the storage of a List<FrozenSet<T>>, a Stack, a Queue, an
ImmutableArray or a Collection of them: replacing an element after Old<T>() was not seen.
Measuring the fix found four more defects next to it, which are taken into this change.

What changes

  • Old<T>() copies an array whose element type is declared in System.Collections.Frozen. Its
    elements keep their own rule: a FrozenSet<T> element is still shared.
  • A value of a value type declared in System.Collections.Frozen (the two public enumerators, a
    struct or enum of yours declared there) is copied and compared like any other value type. Before,
    it was compared by reference to its box and always reported as changed.
  • When the declared T of EnsureAssignable<T>() is an array type or System.Array, the two
    values are compared as a whole, element by element. Assignable patterns do not apply and are not
    examined. Before, the members of Array were compared, so two arrays of equal length and
    different content passed.
  • Two arrays are equal only when they have the same rank, and the same length and lower bound in
    every dimension. Before, a 1 by 2 and a 2 by 1 array of the same elements were equal.
  • An array of pointers or of function pointers is compared by address. Old<T>() and a comparison
    no longer throw for an object that holds one (NotSupportedException for pointers;
    BadImageFormatException for function pointers, also inside another array or a List).

FrozenSet<T>, FrozenDictionary<TKey, TValue> and every other class declared in the namespace
stay shared.

Limits, stated in the API reference with the ways out:

  • Only the declared T selects the comparison as a whole: two arrays passed as object or as an
    interface are compared by the members that T shows (ICollection shows Count only).
  • A lower bound counts; element types count through boxing (int[] equals object[] of the same
    numbers, not long[]).
  • Under Native AOT the annotation on T does not preserve the members of the element type of an
    array as T.
  • An array as T is compared element by element, where such a call read no element before. One
    measurement, Debug build: two byte[] of 1,000,000 equal elements allocated about 81 MB.
  • The copy of a dictionary or set whose keys are arrays, with its default comparer, cannot be
    looked up by key.
  • An object that holds a FrozenSet<T> and an enumerator of it: measured on .NET 8, a change
    inside an element of the set is reported at the enumerator member. The result depends on the
    runtime.

Verification

  • 994 unit tests (746 before), Debug and Release.
  • Native AOT win-x64: 84 checks in the default publish and 83 with the hidden dictionary entry,
    with postconditions on and with DBC_POST=off; no failure.
  • Mutation checks by hand: each of the four changes taken out in turn; the tests that should fail
    do (30, 45, 38 and 65).
  • 378 measured calls of the design agree with the expected values on the final code.
  • Not run locally: CI on ubuntu-latest and the Linux Native AOT smoke run. Not measured: Native
    AOT for arrays of pointers and of function pointers.

Records: ADR-0022, ADR-0012, ADR-0021 and ADR-0007 carry amendments. CHANGELOG [Unreleased]
gains one BREAKING entry (rank, lengths and lower bounds) and one Fixed entry (arrays of
pointers); the entries on values compared as a whole and on the frozen collections are extended.
The version does not change.

Merge: squash. The 21 commits are seven changes with their review corrections; the first
commit alone changes an array passed as T and the second repairs it.

Follow-ups: #61 (EnsureImmutableCollection<T>() and arrays of function pointers), #62
(statements of the user documents and ADRs, older than this change, that the code does not bear
out). #40 keeps the declared types that are not array types.

Closes #57.

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.

Old shares an array of frozen collections instead of copying it

1 participant