Skip to content

Add Support for Indexers and Ranges #605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 41 commits into
base: draft-v8
Choose a base branch
from

Conversation

RexJaeschke
Copy link
Contributor

@RexJaeschke RexJaeschke commented Aug 7, 2022

Before you look at the detailed edits, it likely will be useful to read the following:

  • 14.7 Properties|14.7.1 General, which defines countable as it pertains to a type.
  • 14.(x) Indexable sequences, which defines indexable sequence.

@RexJaeschke RexJaeschke marked this pull request as draft August 7, 2022 14:15
@RexJaeschke RexJaeschke added this to the C# 8.0 milestone Aug 7, 2022
@RexJaeschke RexJaeschke changed the base branch from draft-v7 to draft-v8 August 8, 2022 13:05
@RexJaeschke RexJaeschke changed the title Indexers and ranges Add Support for Indexers and Ranges Aug 8, 2022
@RexJaeschke
Copy link
Contributor Author

RexJaeschke commented Aug 11, 2022

[I raised the questions below on https://github.com/dotnet/docs/issues/30512, and got replies, which I need to factor in to this Draft PR.]

I'm transforming this proposal into a Draft PR for the C# Standard, and have several questions:

Q1. Optional Members in Index and Range: The proposal suggests that some members in these types are optional, such as Range's StartAt, EndAt, and All. Which members are required for these types? Why not require all those provided by MS's implementation?

Specifically, the MS proposal states:

The .. syntax for System.Range will require the System.Range type, as well as one or more of the following members:

namespace System
{
    public readonly struct Range
    {
        public Range(System.Index start, System.Index end);
        public static Range StartAt(System.Index start);
        public static Range EndAt(System.Index end);
        public static Range All { get; }
    }
}

The .. syntax allows for either, both, or none of its arguments to be absent. Regardless of the number of arguments, the Range constructor is always sufficient for using the Range syntax. However, if any of the other members are present and one or more of the .. arguments are missing, the appropriate member may be substituted.

MS's feedback on my Q was:

The only one required is the constructor: we will fall back to it if any of the others are missing. As to why it was specified this way, I'm not certain, and unfortunately Andy (who implemented it) just went on leave for a few months.

As a result, I'm omitting any mention of the optional members StartAt, EndAt, and All, and defining things in terms of the constructor.

Q2. Range indexer setter: What if anything should we say about the implicit and explicit setter for a Range indexer? Certainly, one can define a setter for a user-defined type; however, it is not obvious as to what such a setter would do, especially since it must be used on the left-hand side of assignment taking a right-hand side of the same type as the index returns. In the case of type BitArray that would mean something like ba1[range1] = ba2, or perhaps ba1[range1] = ba2[range2]. As far as I can determine, the operations one might like to implement using such a setter are probably best implemented via a named method. In any event, for a compiler-generated Range indexer, attempting to use its setter results in the error message “CS0131 The left-hand side of an assignment must be a variable, property or indexer,” which suggests the generated indexer has no setter. If that is the case, we should say that, perhaps by stressing that the result of a Range indexer is not a variable, so as such, it can't be used on the lhs of an assignment.

MS's feedback on my Q was:

I think we should say nothing about it, or at most say that no setter is synthesized if one does not exist. I think the rest of the rules should just fall out from this, as the error implies.

I chose to say nothing about the setter, which means that attempting to use such a setter results in unspecified behavior.

@RexJaeschke
Copy link
Contributor Author

When adapting the MS proposal, I invented the term indexable sequence. The rationale for that name choice follows.

Various MS-hosted on-line pages use the term sequence. This word is already used quite a bit in the C# spec, in both a general sense as well as being defined in the context of query expressions. §11.17 Query expressions|§11.17.1 General states:

A query expression begins with a from clause and ends with either a select or group clause. The initial from clause may be followed by zero or more from, let, where, join or orderby clauses. Each from clause is a generator introducing a range variable that ranges over the elements of a sequence. Each let clause introduces a range variable representing a value computed by means of previous range variables. ….

That definition is not applicable to indexes and ranges! The MS-provided proposal uses collection; however, that implies enumerable support, which is not required by indexes and ranges. (BTW, although it is used a lot in the C# spec, the term collection is not defined!) As such, rather than overload an existing term or invent a completely different one, I came up with indexable sequence.

@RexJaeschke
Copy link
Contributor Author

The MS proposal, section “Implicit Range support,” provided a very detailed discussion of how to transform a pair of Indexes into a call to Slice depending on the form of the range used. I did not retain this in the final proposal, as I saw no point in doing so. Given a start and end index, it is a simple matter to compute the length in all cases regardless of range format!, as I show in my range indexer implementation in “Explicit range support.”

@BillWagner BillWagner force-pushed the indexers-and-ranges branch 2 times, most recently from c810a4a to 867ba90 Compare February 6, 2023 13:27
@RexJaeschke RexJaeschke added the type: feature This issue describes a new feature label Jul 22, 2023
Copy link
Member

@BillWagner BillWagner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed everything but classes.md.

Comment on lines +647 to +650
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check the normative language as well, but this type seems to be an implementation specific choice. Are we going to standardize that this is the method to retrieve a range in an array?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call on 2025-02-19: let's not specify this, but instead, where it's currently referenced, we should specify the result. We don't want this to be "implementation-defined" (as we don't want implementations to give their methods). We will want to make it clear that it is a shallow copy, and permit but not require that the same reference to an empty array can be returned by multiple calls.

@@ -379,18 +379,44 @@ The following types, including the members listed, shall be defined in a conform

A conforming implementation may provide `Task.GetAwaiter()` and `Task<TResult>.GetAwaiter()` as extension methods.

> Note to TG2 reviewers: Required vs. Optional library type members: We need to indicate which members of `Index` and `Range` are required and which are optional.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first thought is to keep it simple. I'd just leave them all. I don't have a super strong opinion, but that's my first thought.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for comparison, here's what we say about String:

    public sealed class String : IEnumerable<Char>, IEnumerable
    {
        public int Length { get; }
        public char this [int index] { get; }
        public static string Format(string format, params object?[] args);
    }

We don't define IEquatable<T> anywhere, so arguably we shouldn't use it (although that's already present for ValueTask)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C.1 states: "The standard library is intended to be the minimum set of types and members required by a conforming C# implementation. As such, it contains only those members that are explicitly required by the C# language specification."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if everyone else is happy being maximal, let's do that.

Comment on lines +9 to +12
Console.WriteLine("ba1[98] = {0}", ba1[98]); // False
Console.WriteLine("ba1[Index 0] = {0}", ba1[new Index(0)]); // True
Console.WriteLine("ba1[^1] = {0}", ba1[^1]); // True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have them, I'd use interpolation expressions:

Suggested change
Console.WriteLine("ba1[0] = {0}", ba1[0]); // True
Console.WriteLine("ba1[98] = {0}", ba1[98]); // False
Console.WriteLine("ba1[Index 0] = {0}", ba1[new Index(0)]); // True
Console.WriteLine("ba1[^1] = {0}", ba1[^1]); // True
Console.WriteLine($"ba1[0] = {ba1[0]}"); // True
Console.WriteLine($"ba1[98] = {ba1[98]}"); // False
Console.WriteLine($"ba1[Index 0] = {ba1[new Index(0)]}"); // True
Console.WriteLine($"ba1[^1] = {ba1[^1]}"); // True

ba[3] = true;
ba[^1] = true;
Console.WriteLine("ba = >{0}<", ba);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment:

Suggested change
Console.WriteLine("ba = >{0}<", ba);
Console.WriteLine($"ba = >{ba}<");

ba[3] = true;
ba[^1] = true;
Console.WriteLine("ba = >{0}<", ba);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again:

Suggested change
Console.WriteLine("ba = >{0}<", ba);
Console.WriteLine($"ba = >{ba}<");

> Console.WriteLine(values[1..3][^1][..2][^1]); // 42
> ```
>
> all the Range and Index expressions in `values[1..3][^1][..2][^1]` are transformed by the implementation, resulting in `values[2][1]`, which is 42. *end example*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is an example, let's consider writing out each transformation: values[1..3] evaluates to .... Then result[^1]evaluates toresult2which is .... Then,result2[..2]evaluates toresult3which is .... Finally,result3[^1]` is 42.

@@ -2256,6 +2339,8 @@ The binding-time processing of an indexer access of the form `P[A]`, where `P` i

Depending on the context in which it is used, an indexer access causes invocation of either the get accessor or the set accessor of the indexer. If the indexer access is the target of an assignment, the set accessor is invoked to assign a new value ([§12.21.2](expressions.md#12212-simple-assignment)). In all other cases, the get accessor is invoked to obtain the current value ([§12.2.2](expressions.md#1222-values-of-expressions)).

> *Note*: An implementation is required to provide an instance indexer member with a single parameter of type `Index` for any type that meets the criteria specified in §indexable-sequence-impl-support-for-index. An implementation is required to provide an instance indexer member with a single parameter of type `Range` for any type that meets the criteria specified in §indexable-sequence-impl-support-for-range. *end note*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is required, should it be normative? Should we state that the compiler must translate those expressions into the equivalent expressions using the integral types?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should probably be rewritten anyway, in terms of "behaving as if an instance indexer member is provided".
(Again, this is up for discussion - basically, do we expect that to be in the emitted IL, like a default instance constructor is? I suspect not.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Note contradicts the clauses it references. With @jskeet as to the need to rewrite (normatively) here. Whether it describes “as if an indexer” exists (as in the referenced clauses), an argument conversion (as if) some call to GetOffset() is called (as in the changes currently suggested to array indexing), or as some kind of “target type conversion” (as here) is up for discussion. Hopefully we can describe array and non-array indexing behaviour very similarly.

@@ -3557,6 +3643,47 @@ The result of evaluating `~x`, where `X` is an expression of an enumeration ty

Lifted ([§12.4.8](expressions.md#1248-lifted-operators)) forms of the unlifted predefined bitwise complement operators defined above are also predefined.

### §index-from-end-operator Index-from-end operator
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhere in this section (perhaps in a note), we should state that ^0 represents the end of the sequence, not the last element. ^1 represents the last element. (Yes, I know it's in a comment in the example, but still...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that should be explained where System.Index is introduced or here, with x-refs between them.

Comment on lines +3823 to +3826
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is triggering the left recursive issue in the grammar. I defer to @Nigel-Ecma for a suggestion on an alternative expression.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I don't think this grammar is correct. Would it permit something like P[0..5..8..^2]?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, the grammar is wrong, see later comment.

Comment on lines +3838 to +3840
- `s .. e` is transformed by the implementation to `new System.Range(s, e)`.
- `s ..` is transformed by the implementation to `new System.Range(s, ^0)`.
- `.. e` is transformed by the implementation to `new System.Range(0, e)`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style nit. Let's omit the spaces here:

Suggested change
- `s .. e` is transformed by the implementation to `new System.Range(s, e)`.
- `s ..` is transformed by the implementation to `new System.Range(s, ^0)`.
- `.. e` is transformed by the implementation to `new System.Range(0, e)`.
- `s..e` is transformed by the implementation to `new System.Range(s, e)`.
- `s..` is transformed by the implementation to `new System.Range(s, ^0)`.
- `..e` is transformed by the implementation to `new System.Range(0, e)`.

Copy link
Member

@BillWagner BillWagner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments on the classes.md spec updates.


An ***indexable sequence*** is an ordered set of zero or more elements having the same type. Any given element can be accessed via an index, and a contiguous subset of elements—referred to as a ***slice***—can be denoted via a range.

An index is represented by a read-only variable of the value type `System.Index`. A range is represented by a read-only variable of value type `System.Range`, which contains a start and end index. A slice of an array is represented by a (possibly empty) array. The representation of a slice of a user-defined type is determined by the implementer of that type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Should this be a countable type?

Suggested change
An index is represented by a read-only variable of the value type `System.Index`. A range is represented by a read-only variable of value type `System.Range`, which contains a start and end index. A slice of an array is represented by a (possibly empty) array. The representation of a slice of a user-defined type is determined by the implementer of that type.
An index is represented by a read-only variable of the value type `System.Index`. A range is represented by a read-only variable of value type `System.Range`, which contains a start and end index. A slice of an array is represented by a (possibly empty) array. The representation of a slice of a countable type that is not an array type is determined by the implementer of that type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does a slice have to be of a countable type? You can write an uncountable type with an indexer accepting a range, and make it fail at execution time if either end of the range is "from the end".

I agree it will usually be of a countable type, just trying to explore...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jskeet – In C# terms a slice made from a Range has to be of a countable type as Range is built upon Index, and Index requires a length for it to be converted into an integer offset…However a non-countable type could provide the same underlying Slice method…

Looking at your query made me think how careful we have to be with terms here.

An index is represented by a read-only variable of the value type System.Index.

No, an indexer takes an index as its argument, which does not need to be of type Index, or even int. So “…index is represented by…” is wrong, we’re using “index” to both mean a value of any type used as an index and to mean the (int, bool) pair embodied by System.Index

I think we might yet need a lot of careful wordsmithing. Drat!


For an indexable sequence of length *N*, elements can be accessed using indexes 0 through *N-1*, which are relative to the start. Elements can also be accessed relative to the end via the `^` index-from-end operator (§index-from-end-operator). `^0` denotes the (non-existent) element just beyond the end.

A slice can be obtained using the `..` range operator (§range-operator). A range of the form `s..e` starts at element `s` and ends with the element immediately prior to element `e`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine as is. The other forms of .. are in the linked section.


A slice can be obtained using the `..` range operator (§range-operator). A range of the form `s..e` starts at element `s` and ends with the element immediately prior to element `e`.

All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.11.2](expressions.md#128112-array-access).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might consider it a note:

Suggested change
All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.11.2](expressions.md#128112-array-access).
All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.11.2](expressions.md#128112-array-access).
> *Note*: Jagged arrays ([§17.1](arrays.md#171-general)) are arrays of single-dimensional arrays, and are therefore indexable. *end note*.


An object of type `string` is an indexable sequence.

A user-defined type can provide explicit support for indexer access ([§12.8.11.3](expressions.md#128113-indexer-access)) using `System.Index` and `System.Range`. (See §indexable-sequence-expl-support-for-index and §indexable-sequence-expl-support-for-range.) If various criteria are met, an existing user-defined type that does *not* have such explicit support, shall have provided for it by the implementation implicit support for such indexer and range access. (See §indexable-sequence-impl-support-for-index and §indexable-sequence-impl-support-for-range.) In both cases, the type is recognized as being an indexable sequence type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like Joseph's wording here.


#### §indexable-sequence-expl-support-for-index Explicit Index support

A type having an instance indexer taking a single argument of type `System.Index`, or a first argument of that type followed by optional arguments, may be indexed as described by [§12.8.11.3](expressions.md#128113-indexer-access).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the change be this simple:

Suggested change
A type having an instance indexer taking a single argument of type `System.Index`, or a first argument of that type followed by optional arguments, may be indexed as described by [§12.8.11.3](expressions.md#128113-indexer-access).
A type having an instance indexer taking a single argument with an implicit conversion to `System.Index`, or a first argument with an implicit conversion to `System.Index` followed by optional arguments, may be indexed as described by [§12.8.11.3](expressions.md#128113-indexer-access).


A user-defined type can provide explicit support for indexer access ([§12.8.11.3](expressions.md#128113-indexer-access)) using `System.Index` and `System.Range`. (See §indexable-sequence-expl-support-for-index and §indexable-sequence-expl-support-for-range.) If various criteria are met, an existing user-defined type that does *not* have such explicit support, shall have provided for it by the implementation implicit support for such indexer and range access. (See §indexable-sequence-impl-support-for-index and §indexable-sequence-impl-support-for-range.) In both cases, the type is recognized as being an indexable sequence type.

### §indexable-sequence-support-for-index Providing support for Index
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For both this section and the corresponding ranges section, I propose we rework them to introduce a pattern based description, following the format used for async return types, LINQ, and so on.

The concept is that a countable sequence has a certain pattern for indexer, Count, Length, etc.

A type can provide support for ranges and index operations by implementing a pattern: Indexer that takes System.Index or in System.Index etc. Slice method that takes a System.Range. Then describe ref variants.

Finally, for arrays and other types the compiler recognizes as countable, the compiler can synthesize the lowering of the indexer operations.


A slice can be obtained using the `..` range operator (§range-operator). A range of the form `s..e` starts at element `s` and ends with the element immediately prior to element `e`.

All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.11.2](expressions.md#128112-array-access).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array access is now 12.8.12.2:

Suggested change
All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.11.2](expressions.md#128112-array-access).
All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.12.2](expressions.md#128122-array-access).


An object of type `string` is an indexable sequence.

A user-defined type can provide explicit support for indexer access ([§12.8.11.3](expressions.md#128113-indexer-access)) using `System.Index` and `System.Range`. (See §indexable-sequence-expl-support-for-index and §indexable-sequence-expl-support-for-range.) If various criteria are met, an existing user-defined type that does *not* have such explicit support, shall have provided for it by the implementation implicit support for such indexer and range access. (See §indexable-sequence-impl-support-for-index and §indexable-sequence-impl-support-for-range.) In both cases, the type is recognized as being an indexable sequence type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexer access moved:

Suggested change
A user-defined type can provide explicit support for indexer access ([§12.8.11.3](expressions.md#128113-indexer-access)) using `System.Index` and `System.Range`. (See §indexable-sequence-expl-support-for-index and §indexable-sequence-expl-support-for-range.) If various criteria are met, an existing user-defined type that does *not* have such explicit support, shall have provided for it by the implementation implicit support for such indexer and range access. (See §indexable-sequence-impl-support-for-index and §indexable-sequence-impl-support-for-range.) In both cases, the type is recognized as being an indexable sequence type.
A user-defined type can provide explicit support for indexer access ([§12.8.12.3](expressions.md#128123-indexer-access)) using `System.Index` and `System.Range`. (See §indexable-sequence-expl-support-for-index and §indexable-sequence-expl-support-for-range.) If various criteria are met, an existing user-defined type that does *not* have such explicit support, shall have provided for it by the implementation implicit support for such indexer and range access. (See §indexable-sequence-impl-support-for-index and §indexable-sequence-impl-support-for-range.) In both cases, the type is recognized as being an indexable sequence type.


#### §indexable-sequence-expl-support-for-range Explicit Range support

A type having an instance indexer taking a single argument of type `System.Range`, or a first argument of that type followed by optional arguments, may be indexed as described by [§12.8.11.3](expressions.md#128113-indexer-access).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

section change:

Suggested change
A type having an instance indexer taking a single argument of type `System.Range`, or a first argument of that type followed by optional arguments, may be indexed as described by [§12.8.11.3](expressions.md#128113-indexer-access).
A type having an instance indexer taking a single argument of type `System.Range`, or a first argument of that type followed by optional arguments, may be indexed as described by [§12.8.12.3](expressions.md#128123-indexer-access).


- The type is countable ([§15.7.1](classes.md#1571-general)).
- The type has an accessible instance method named `Slice` taking two arguments of type `int` as the only arguments. For type `string`, the method `Substring` is used instead of `Slice`.
> *Note*: As specified in [§12.8.11.2](expressions.md#128112-array-access), for array access, the method `System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray` is used instead of `Slice`. *end note*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

section change:

Suggested change
> *Note*: As specified in [§12.8.11.2](expressions.md#128112-array-access), for array access, the method `System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray` is used instead of `Slice`. *end note*
> *Note*: As specified in [§12.8.12.2](expressions.md#128122-array-access), for array access, the method `System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray` is used instead of `Slice`. *end note*

@jskeet jskeet added the meeting: discuss This issue should be discussed at the next TC49-TG2 meeting label Feb 12, 2025
@RexJaeschke
Copy link
Contributor Author

I just added a new commit that prohibits an argument of type Index in the argument_list of an initializer_target.

I am working on the spec for a V13 feature (for which there appears to be no formal MS spec, but see here). That spec removes a constraint that was apparently added in V8 as a result of the addition of support for Index, but which was not previously mentioned in this V8 proposal.

Consider the following:

    public class Program
    {
        public static void Main()
        {
            Index idx = 0;
            var c = new C()
            {
                buffer = { [0] = 8, [1] = 4 }               // V6+ allows
//                buffer = { [(Index)0] = 8, [1] = 4 } // requires V13
//                buffer = { [idx] = 8, [1] = 4 }         // requires V13
//                buffer = { [^1] = 4, [0] = 8 },        // requires V13
            };
        }
    }
}
public class C
{
    public int[] buffer = new int[2];
}

V6 added support for the initialization of buffer using initializer_target notation [...] = exp. However, when V8 added support for System.Index it did not allow the use of a target having that type, so the 3 commented-out initializations are disallowed. V13 lifts that restriction.

@KalleOlaviNiemitalo
Copy link
Contributor

when V8 added support for System.Index it did not allow the use of a target having that type

Does that even disallow initialisation like new Dictionary<Index, int>() { [^0] = 0 }, where the Index is not translated?

@RexJaeschke
Copy link
Contributor Author

@KalleOlaviNiemitalo Interestingly, your example compiles starting with V8, so my recent fix probably needs tweaking. Let's compare your example with mine:

var d = new Dictionary<Index, int>() { [^0] = 0 };                            // V8+ allows
var c = new C()                      { buffer = { [^1] = 4, [0] = 8 } };      // requires V13

In the case of c, the Index expression is inside a nested object_initializer.

Here's the draft-v8 grammar both are (supposedly) following:

object_initializer
    : '{' member_initializer_list? '}'
    | '{' member_initializer_list ',' '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : initializer_target '=' initializer_value
    ;

initializer_target
    : identifier
    | '[' argument_list ']'
    ;

initializer_value
    : expression
    | object_or_collection_initializer
    ;

More work is needed!

Copy link
Contributor

@Nigel-Ecma Nigel-Ecma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven’t (yet) considered @BillWagner’s suggestion to rewrite the clauses describing emulating Index and Range indexing; and I ran out of steam a bit at the end 😉 but I'll post so it’s in before the next meeting.

Quite a few changes, some will be debated or just plain wrong…


### §indexable-sequence-general General

An ***indexable sequence*** is an ordered set of zero or more elements having the same type. Any given element can be accessed via an index, and a contiguous subset of elements—referred to as a ***slice***—can be denoted via a range.
Copy link
Contributor

@Nigel-Ecma Nigel-Ecma Feb 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref: #1250 a Proposal to Define Some Container-Related Terms

I think this definition of indexable sequence (or collection) is not right.

#1250 has a renaming and different definition:

An indexable collection is a collection in which any given element can be accessed via an index, and a contiguous subset of elements—referred to as a slice—can be denoted via a range.

#1250 also defines collection:

A collection is an ordered set of zero or more collection elements that can be used in a polymorphic way. Often, the elements are of the same data type such as int or string. Sometimes the items derive from a common type; even deriving from the most general type, object.

I”m not sure where the “polymorphic” in the collection definition comes from, but it does correctly drop the “same type” of indexable sequence.

The term “ordered set” allows the reading that it is the elements that are ordered (as in [partially] ordered sets and SortedSet<T>), which they need not be. It is the indicies that are ordered… usually.

System.Index indicies by themselves are actually not ordered and the type does not implement IComparable<Index> just IEquatable<Index>. They are ordered only w.r.t a particular collection, which has some Length, this is because indicies can be end-relative.

Maybe:

Suggested change
An ***indexable sequence*** is an ordered set of zero or more elements having the same type. Any given element can be accessed via an index, and a contiguous subset of elements—referred to as a ***slice***—can be denoted via a range.
An ***indexable sequence*** is a set of zero or more elements where any given element can be accessed via an index.
A ***range*** is a contiguous subset of ordered indices, and the subset of elements accessed via the indices in a range is referred to as a ***slice***.

Or something like that…

This is similar to the definition in #1250, but without the underlying definition of collection (for now…).

The definition of range here doesn’t address that a C# a..b Range does not include b, or that a C# Range may not be a contiguous set of ordered indices (consider 1..^Int.MaxValue which is a valid Range to construct but useless to use!).

So there is a disparity between the term “range” and C#’s type Range– that might be confusing…

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest either "sequence" or "collection" instead of "set" as well. Likewise "subsequence" instead of "subset"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Replacement comment, I hit add too quickly]

@jskeet dropping set is a good idea.

But are we taking about sequences, which by (English language) definition are ordered, or collections which are not?

Do we need to make it clear that any ordering here comes from the index type being ordered, not the elements?

Indices do not need to be ordered per se, but a range of indices implies ordered indices, and indexable sequence/collection is being defined to be used by ranges.

Which gives us:

Suggested change
An ***indexable sequence*** is an ordered set of zero or more elements having the same type. Any given element can be accessed via an index, and a contiguous subset of elements—referred to as a ***slice***—can be denoted via a range.
An ***indexable sequence*** is a sequence of zero or more elements where any given element can be accessed via an ordered index.
A ***range*** is a contiguous subset of ordered indices.
A ***slice** is the indexable (sub)sequence of elements selected in index order from an indexable sequence using the indices in a range.

Too detailed/convoluted?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question under discussion: does Dictionary<TKey, TValue> count as an indexable sequence? (Or rather, do we want it to?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting types:

  • int
  • System.Index
  • long (just for arrays)

We may want to limit this to integer-based indexes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a Dictionary<int, string>?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also had some discussion about what "ordered index" means (especially when System.Index isn't inherently ordered).

@@ -3299,6 +3299,8 @@ In a *ref_property_body* an expression body consisting of `=>` followed by `ref`

When a property declaration includes an `extern` modifier, the property is said to be an ***external property***. Because an external property declaration provides no actual implementation, each of the *accessor_body*s in its *accessor_declarations* shall be a semicolon.

A type is ***countable*** if it has an instance property named `Length` or `Count` with an accessible `get` accessor ([§15.7.3]( classes.md#1573-accessors)) and a return type of `int`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm…

struct Plank
{
   int Length { get; }      // all measurements in mm
   int Width { get; }
   int Thickness { get; }
}

A single plank is countable (in the sense intended)?

Maybe:

Suggested change
A type is ***countable*** if it has an instance property named `Length` or `Count` with an accessible `get` accessor ([§15.7.3]( classes.md#1573-accessors)) and a return type of `int`.
A collection type is ***countable*** if it has an instance property named `Length` or `Count` with an accessible `get` accessor ([§15.7.3]( classes.md#1573-accessors)) and a return type of `int`.

with a x-ref to wherever collection gets defined (see #1250).

Note: Qualifying, “collection type”, also avoids integers not be countable – which could otherwise confuse the numerate 😉

Note: This doesn’t address “In case both Length and Count are present, Length will be preferred.” found here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While having general terms is good this particular term currently only exists to be referenced in the new clauses “Implicit Index support” and ”Range Index support”. It would be better to move this to down to those two clauses, possibly even removing it as defined term altogether.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to the general clause of indexable sequences, where we're introducing other terms that are only relevant to that. (This doesn't address the plank example, but at least suggests where countable types are relevant.)


An index is represented by a read-only variable of the value type `System.Index`. A range is represented by a read-only variable of value type `System.Range`, which contains a start and end index. A slice of an array is represented by a (possibly empty) array. The representation of a slice of a user-defined type is determined by the implementer of that type.

For an indexable sequence of length *N*, elements can be accessed using indexes 0 through *N-1*, which are relative to the start. Elements can also be accessed relative to the end via the `^` index-from-end operator (§index-from-end-operator). `^0` denotes the (non-existent) element just beyond the end.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jnm2 – 0 through N for ^ to allow for ^0? Otherwise 👍


For an indexable sequence of length *N*, elements can be accessed using indexes 0 through *N-1*, which are relative to the start. Elements can also be accessed relative to the end via the `^` index-from-end operator (§index-from-end-operator). `^0` denotes the (non-existent) element just beyond the end.

A slice can be obtained using the `..` range operator (§range-operator). A range of the form `s..e` starts at element `s` and ends with the element immediately prior to element `e`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, a slice might be obtained using a range – or it might fail, a range can be constructed using the range operator…

TBD: Wordsmith a suggestion


A slice can be obtained using the `..` range operator (§range-operator). A range of the form `s..e` starts at element `s` and ends with the element immediately prior to element `e`.

All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.11.2](expressions.md#128112-array-access).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @jnm2 here – C# doesn’t have jagged arrays per se, it supports single-dimensional arrays where the element type can be an array which:

can be used to represent “jagged arrays”. [§17.1]

I would just drop the reference (and the !), x-ref fixed as per @BillWagner comment below:

Suggested change
All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.11.2](expressions.md#128112-array-access).
All single-dimensional are indexable sequences; multi-dimensional arrays are not. The use of indexes and ranges with arrays is described in [§12.8.12.2](expressions.md#128122-array-access).

An implementation shall behave as if it provides a non-virtual instance indexer member with a single parameter of type `System.Index` for any type that meets the following criteria:

- The type is countable ([§15.7.1](classes.md#1571-general)).
- The type has an accessible instance indexer taking an argument of type `int` as its only argument.
Copy link
Contributor

@Nigel-Ecma Nigel-Ecma Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Replaced] (My fears were justified)

For the refness question, maybe:

The type has an accessible instance indexer taking a single parameter of type int without any associated parameter_mode_modifier.


The provided instance indexer shall have the same get and set members with matching accessibility as the `int` indexer.

The provided instance indexer shall take the given `System.Index` and use that to call the instance indexer taking an `int`. If both the `Length` and `Count` properties exist and are accessible, `Length` is used.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting descriptive to specification language:

Suggested change
The provided instance indexer shall take the given `System.Index` and use that to call the instance indexer taking an `int`. If both the `Length` and `Count` properties exist and are accessible, `Length` is used.
The provided instance indexer shall take the given `System.Index` and use that to call the instance indexer taking an `int`. If both the `Length` and `Count` properties exist and are accessible, `Length` shall be used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further to my last comment, there is no context here as to why either Length or Count should be used at all – there is no mention of GetOffset is there is in expressions.md line 2242. And as commented on the latter, surely we don't need this stuff in two places?


A type having an instance indexer taking a single argument of type `System.Range`, or a first argument of that type followed by optional arguments, may be indexed as described by [§12.8.11.3](expressions.md#128113-indexer-access).

> *Example*: In [§15.9](classes.md#159-indexers), there is an example defining type `BitArray`, which stores bits in an array of `int`. Adding a `Range` indexer that returns a `BitArray` representing the bit slice designated by the Range, is simple:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jnm2 – maybe “straightforward” which doesn’t carry the same risk of offense as “simple”, or may be:

Suggested change
> *Example*: In [§15.9](classes.md#159-indexers), there is an example defining type `BitArray`, which stores bits in an array of `int`. Adding a `Range` indexer that returns a `BitArray` representing the bit slice designated by the Range, is simple:
> *Example*: In [§15.9](classes.md#159-indexers), there is an example defining type `BitArray`, which stores bits in an array of `int`. Adding a `Range` indexer that returns a `BitArray` representing the bit slice designated by the `Range` may be achieved as follows:

Wordsmith away folks!

Comment on lines +5584 to +5585
- The type has an accessible instance method named `Slice` taking two arguments of type `int` as the only arguments. For type `string`, the method `Substring` is used instead of `Slice`.
> *Note*: As specified in [§12.8.11.2](expressions.md#128112-array-access), for array access, the method `System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray` is used instead of `Slice`. *end note*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a different method is being used for arrays then it needs to be specified in normative text, not as an informative note.

> *Note*: As specified in [§12.8.11.2](expressions.md#128112-array-access), for array access, the method `System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray` is used instead of `Slice`. *end note*
- The type does not have an accessible instance indexer taking a `System.Range` as its only argument, or as its first argument with the remaining arguments being optional.

The provided instance indexer shall have the same accessibility and return type, including `ref` if present, as `Slice`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ref_kind, I suspect it just predates this?

Suggested change
The provided instance indexer shall have the same accessibility and return type, including `ref` if present, as `Slice`.
The provided instance indexer shall have the same accessibility and return type, including any *ref_kind* if present, as `Slice`.

Copy link
Contributor

@jskeet jskeet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stopped reviewing after getting confused at range_expression...


### §indexable-sequence-general General

An ***indexable sequence*** is an ordered set of zero or more elements having the same type. Any given element can be accessed via an index, and a contiguous subset of elements—referred to as a ***slice***—can be denoted via a range.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest either "sequence" or "collection" instead of "set" as well. Likewise "subsequence" instead of "subset"?


An ***indexable sequence*** is an ordered set of zero or more elements having the same type. Any given element can be accessed via an index, and a contiguous subset of elements—referred to as a ***slice***—can be denoted via a range.

An index is represented by a read-only variable of the value type `System.Index`. A range is represented by a read-only variable of value type `System.Range`, which contains a start and end index. A slice of an array is represented by a (possibly empty) array. The representation of a slice of a user-defined type is determined by the implementer of that type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does a slice have to be of a countable type? You can write an uncountable type with an indexer accepting a range, and make it fail at execution time if either end of the range is "from the end".

I agree it will usually be of a countable type, just trying to explore...


A slice can be obtained using the `..` range operator (§range-operator). A range of the form `s..e` starts at element `s` and ends with the element immediately prior to element `e`.

All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.11.2](expressions.md#128112-array-access).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also say that these arrays are countable as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jskeet – I’ve now added that to the suggested definition of slice.


All single-dimensional and jagged arrays ([§17.1](arrays.md#171-general)) are indexable sequences; multi-dimensional arrays are not! The use of indexes and ranges with arrays is described in [§12.8.11.2](expressions.md#128112-array-access).

An object of type `string` is an indexable sequence.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And countable? (I won't keep adding this...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jskeet – Once we’ve the definition of indexable narrowed down to what is needed for this feature then countability (if it still exists as a concept) will be implied as using an Index relies on it.


#### §indexable-sequence-expl-support-for-index Explicit Index support

A type having an instance indexer taking a single argument of type `System.Index`, or a first argument of that type followed by optional arguments, may be indexed as described by [§12.8.11.3](expressions.md#128113-indexer-access).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from anything else: I think these should refer to parameters, not arguments. There's no such thing as an optional argument, only an optional parameter.

@@ -2256,6 +2339,8 @@ The binding-time processing of an indexer access of the form `P[A]`, where `P` i

Depending on the context in which it is used, an indexer access causes invocation of either the get accessor or the set accessor of the indexer. If the indexer access is the target of an assignment, the set accessor is invoked to assign a new value ([§12.21.2](expressions.md#12212-simple-assignment)). In all other cases, the get accessor is invoked to obtain the current value ([§12.2.2](expressions.md#1222-values-of-expressions)).

> *Note*: An implementation is required to provide an instance indexer member with a single parameter of type `Index` for any type that meets the criteria specified in §indexable-sequence-impl-support-for-index. An implementation is required to provide an instance indexer member with a single parameter of type `Range` for any type that meets the criteria specified in §indexable-sequence-impl-support-for-range. *end note*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should probably be rewritten anyway, in terms of "behaving as if an instance indexer member is provided".
(Again, this is up for discussion - basically, do we expect that to be in the emitted IL, like a default instance constructor is? I suspect not.)

System.Index operator ^(int fromEnd);
```

For this operator, an object of (the immutable struct) type `System.Index` is returned that denotes element number `fromEnd` from the end of any indexable sequence. `^n` is shorthand for `new System.Index(n, fromEnd: true)`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For this operator, an object of (the immutable struct) type `System.Index` is returned that denotes element number `fromEnd` from the end of any indexable sequence. `^n` is shorthand for `new System.Index(n, fromEnd: true)`.
For this operator, a value of (the immutable struct) type `System.Index` is returned that denotes element number `fromEnd` from the end of any indexable sequence. `^n` is shorthand for `new System.Index(n, fromEnd: true)`.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(To avoid any suspicion of boxing.)

;
```

For an operation of the form `s .. e`, binary operator overload resolution ([§12.4.5](expressions.md#1245-binary-operator-overload-resolution)) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator. *range_expression* shall have type `System.Index` or a type that can be converted implicitly to that type. Only one predefined range operator exists:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For an operation of the form `s .. e`, binary operator overload resolution ([§12.4.5](expressions.md#1245-binary-operator-overload-resolution)) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator. *range_expression* shall have type `System.Index` or a type that can be converted implicitly to that type. Only one predefined range operator exists:
For an operation of the form `s .. e`, binary operator overload resolution ([§12.4.5](expressions.md#1245-binary-operator-overload-resolution)) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator. Each of `s` and `e` shall have type `System.Index` or a type that can be converted implicitly to that type. Only one predefined range operator exists:

(Or let's clarify in a different way. The fact that range_expression uses range_expression make this more confusing.)

```ANTLR
range_expression
: unary_expression
| range_expression? '..' range_expression?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this definitely correct? That suggests 0 .. 4 .. 5 would be valid, because 0 is a valid range expression, and so is 4 .. 5.

I think I'm missing something here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven’t yet taken a look at the grammar, but yes that looks wrong.

System.Range operator ..(System.Index start = 0, System.Index end = ^0);
```

The left and right operands denote, respectively, a start and end Index. For this operator, an object of (the immutable struct) type `System.Range` is returned that contains those Indexes. If the left operand is omitted, an Index of `0` is used. If the right operand is omitted, an Index of `^0` is used. As such,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The left and right operands denote, respectively, a start and end Index. For this operator, an object of (the immutable struct) type `System.Range` is returned that contains those Indexes. If the left operand is omitted, an Index of `0` is used. If the right operand is omitted, an Index of `^0` is used. As such,
The left and right operands denote, respectively, a start and end Index. For this operator, a value of (the immutable struct) type `System.Range` is returned that contains those Indexes. If the left operand is omitted, an Index of `0` is used. If the right operand is omitted, an Index of `^0` is used. As such,

@@ -5542,3 +5544,129 @@ When the body of the async function terminates, the return task is moved out of
If the return type of the async function is `void`, evaluation differs from the above in the following way: Because no task is returned, the function instead communicates completion and exceptions to the current thread’s ***synchronization context***. The exact definition of synchronization context is implementation-dependent, but is a representation of “where” the current thread is running. The synchronization context is notified when evaluation of a `void`-returning async function commences, completes successfully, or causes an uncaught exception to be thrown.

This allows the context to keep track of how many `void`-returning async functions are running under it, and to decide how to propagate exceptions coming out of them.

## §indexable-sequence Indexable sequences
Copy link
Contributor

@Nigel-Ecma Nigel-Ecma Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clause & its subclauses might be better after §15.9 Indexers, or as a subclause of §15.9.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Punt on this until the rest is ready.


An index is represented by a read-only variable of the value type `System.Index`. A range is represented by a read-only variable of value type `System.Range`, which contains a start and end index. A slice of an array is represented by a (possibly empty) array. The representation of a slice of a user-defined type is determined by the implementer of that type.

For an indexable sequence of length *N*, elements can be accessed using indexes 0 through *N-1*, which are relative to the start. Elements can also be accessed relative to the end via the `^` index-from-end operator (§index-from-end-operator). `^0` denotes the (non-existent) element just beyond the end.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The definition of indexable sequence, both original and revised versions, do not fix the type of the index. Here the type is fixed to being an integer number (it does not state the actual type).

- The value of `P` is checked to be valid. If the value of `P` is `null`, a `System.NullReferenceException` is thrown and no further steps are executed.
- The value of each expression in the *argument_list* is checked against the actual bounds of each dimension of the array instance referenced by `P`. If one or more values are out of range, a `System.IndexOutOfRangeException` is thrown and no further steps are executed.
- The location of the array element given by the index expression(s) is computed, and this location becomes the result of the array access.
- The index expressions of the *argument_list* are evaluated in order, from left to right. Following evaluation of each index expression, for expressions not having type `System.Index` or `System.Range`, an implicit conversion ([§10.2](conversions.md#102-implicit-conversions)) to one of the following types is performed: `int`, `uint`, `long`, `ulong`. The first type in this list for which an implicit conversion exists is chosen. For instance, if the index expression is of type `short` then an implicit conversion to `int` is performed, since implicit conversions from `short` to `int` and from `short` to `long` are possible. For an index expression having type `System.Index`, the Index is transformed into the corresponding `int` index using `System.Index.GetOffset`, which calculates the offset from the start of the collection using the specified collection length.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this mesh with the “behave as if” behaviour specified in the new clause Implicit Index support (classes.md, line 5594)?

This version mentions GetOffset, but are we requiring that it be called or that an implementation may “behave as if” it is? Elsewhere in the Standard there are passages where behaviour is defined to be the “equivalent of executing the following code” (or words to that effect), would that pattern be better here?


- For an index expression not having type `System.Range`:
- If evaluation of an index expression, the subsequent implicit conversion, or Index transformation causes an exception, then no further index expressions are evaluated, and no further steps are executed.
- The value of `P` is checked to be valid. If the value of `P` is `null`, a `System.NullReferenceException` is thrown and no further steps are executed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check on P being valid is occurring after the conversion of Index to int which requires P

- The value of `P` is checked to be valid. If the value of `P` is `null`, a `System.NullReferenceException` is thrown and no further steps are executed.
- The value of each expression in the *argument_list* is checked against the actual bounds of each dimension of the array instance referenced by `P`. If one or more values are out of range, a `System.IndexOutOfRangeException` is thrown and no further steps are executed.
- The location of the array element given by the index expression(s) is computed, and this location becomes the result of the array access.
- The index expressions of the *argument_list* are evaluated in order, from left to right. Following evaluation of each index expression, for expressions not having type `System.Index` or `System.Range`, an implicit conversion ([§10.2](conversions.md#102-implicit-conversions)) to one of the following types is performed: `int`, `uint`, `long`, `ulong`. The first type in this list for which an implicit conversion exists is chosen. For instance, if the index expression is of type `short` then an implicit conversion to `int` is performed, since implicit conversions from `short` to `int` and from `short` to `long` are possible. For an index expression having type `System.Index`, the Index is transformed into the corresponding `int` index using `System.Index.GetOffset`, which calculates the offset from the start of the collection using the specified collection length.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the valiant attempt to interweave Index and Range is currently a bit hard to follow, well at least I've had to keep re-untangling it as I check the semantics. I think separating out integral, Index and Range cases a bit more would help, I might back it back before the meeting to make a suggestion…

Comment on lines +5596 to +5600
An implementation shall behave as if it provides a non-virtual instance indexer member with a single parameter of type `System.Index` for any type that meets the following criteria:

- The type is countable ([§15.7.1](classes.md#1571-general)).
- The type has an accessible instance indexer taking an argument of type `int` as its only argument.
- The type does not have an accessible instance indexer taking a `System.Index` as its only argument, or as its first argument with the remaining arguments being optional.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think by this list, the following should not error, but it does:

var dict = new X();
dict[^1] = "";

class X : Dictionary<int, string>
{
}

jskeet and others added 2 commits February 19, 2025 21:11

The result of evaluating an array access is a variable of the element type of the array, namely the array element selected by the value(s) of the expression(s) in the *argument_list*.
For single-dimension array access the argument expression can also be of type `System.Index` or `System.Range`, or implicitly convertible to one of these.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an unexpected example:

public struct Int32OrRange
{
    public static implicit operator int(Int32OrRange index) => 0;
    public static implicit operator Range(Int32OrRange index) => new Range(0, 0);
}

string[] array = { };
Int32OrRange x = new();
var result = array[x];

That compiles, and uses the conversion to int. If you remove the conversion, it still compiles, and uses the conversion to Range. I'd have expected the first to compile because neither int nor Range is better than the other.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked with the compiler team. The roslyn implementation ranks single argument indexers in the following order:

  • int
  • uint
  • Int64
  • UInt64
  • Index
  • Range

I wouldn't expect that to change, so we'll have to come up with normative rules that match that behavior.

Co-authored-by: Joseph Musser <[email protected]>
Comment on lines +3842 to +3845
- `s .. e` is transformed by the implementation to `new System.Range(s, e)`.
- `s ..` is transformed by the implementation to `new System.Range(s, ^0)`.
- `.. e` is transformed by the implementation to `new System.Range(0, e)`.
- `..` is transformed by the implementation to `new System.Range(0, ^0)`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“transformed by the implementation” is wrong here as:

  • there is no requirement that the implementation does this transformation (and Roslyn does not always do so); and
  • the Standard uses phrasing like “has the same meaning as” or “is equivalent to” (e.g. §12.8.8, §13.9.5)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
meeting: discuss This issue should be discussed at the next TC49-TG2 meeting Review: in progress at least one person is reviewing this type: feature This issue describes a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants