# C# Language Design Meeting for November 15th, 2023

## Agenda

- [`params` improvements](#params-improvements)
- [Nullability analysis of collection expressions](#nullability-analysis-of-collection-expressions)
- [Evaluation of implicit indexers in object initializers](#evaluation-of-implicit-indexers-in-object-initializers)

## Quote of the Day

- "That is a divide by object error"

## Discussion

### `params` improvements

https://github.com/dotnet/csharplang/issues/7700  
https://github.com/dotnet/csharplang/pull/7661

We started today by reviewing the latest proposal for improving `params`. This has morphed several times over the past few years; previous issues included
https://github.com/dotnet/csharplang/issues/179 and https://github.com/dotnet/csharplang/issues/1757; this proposal encompasses both of these, and tries to
unify with the C# 12 feature collection expressions. The general driving principle of this proposal, and how the LDM is thinking of the feature, is that if
the user can make a collection of the type, they should be able to mark it `params`. This may require some spec work between the two features to extract out
common elements without requiring invasive changing of everywhere that `params` exists today, but it's a good driving principle.

Overall, we are very interested in the feature. It's a rare example of something that both adds expressivity to C# while actually _simplifying_ the language:
with this change and a bit of spec work, we have a much simpler story around collections in the language. You can deconstruct them via pattern matching,
index into them, construct them via collection expressions, and provide implicit construction via `params`. We consider whether we could simply not do this
feature and let collection expressions fill the gap, but there's one clear problem we need to solve: the BCL would like to add `params (ReadOnly)Span` overloads
to many APIs to make them more efficient. This isn't a gap that can be solved by collection expressions, and we think that, if we're going to make a change to
`params`, we should do the entire feature to make reasoning about collections in the language easier, rather than just changing the special cases users need
to think about.

We also considered some ref-struct specific design questions. In terms of allocations, we think the right approach is to again align with collection expressions;
if a collection expression wrapping the arguments passed to a `params Span` allocates, then the expanded invocation form should behave the same. We left a lot
of leeway in the language for optimizations here, so we think continuing to keep to the same guarantees is the obvious thing to do. It also helps prevent
surprise behavioral differences if a user passes a collection expression to a `params` parameter vs calling in expanded form. The other question we considered
is whether to have the `params` parameter be `scoped` by default. Some members were concerned that `params` is not an obvious enough indicator of `scoped`ness,
but we have somewhat already crossed this bridge with `out` parameters. We also don't have any current scenarios that need an unscoped `params` parameter, only
ones that need `scoped`. Given that, we think we should start with `scoped` by default, and let feedback inform whether we've made the right decision.

Finally, we noticed that the overload resolution tiebreaking rules may be missing a few clauses that collection expressions have around non-ref struct comparisons,
so we'll make sure that's in the spec if needed.

#### Conclusion

Proposal is accepted. We will follow the same allocation guarantess as collection expressions, and `ref struct` `params` parameters will be `scoped` by default.

### Nullability analysis of collection expressions

https://github.com/dotnet/csharplang/issues/5354  
https://github.com/dotnet/csharplang/issues/7626

This issue came up late in the design cycle and presented somewhat of a challenge to the compiler. In particular, some types may violate what we'd otherwise
consider an idiomatic implementation, having an `Add` method that allows `T?` while the collection iteration type is `T`. While we don't think that's a totally
unreasonable pattern, it does generally violate our pre-existing rules for collection expressions. For example, if a collection iteration type is `T1`, we don't
allow using an unrelated `T2` as an expression, even if the collection type technically has an `Add(T2)` method. This differs from collection initializers, and
we think that we should carry that difference through here. This also means that any collection expression that are used with older collection types that only
implement `IEnumerable` will not get nullability warnings; this is because `IEnumerator.Current` is unannotated. Given that we already skip nullability warnings
there for `foreach`, we're ok with skipping them on construction as well.

#### Conclusion

We will do nullability analysis based on the iteration type of the collection expression.

### Evaluation of implicit indexers in object initializers

https://github.com/dotnet/csharplang/issues/7684

This is more of a bugfix clarification to the range feature in C# 8. For scenarios where an indexer is used in a nested member initializer, we need to decide
what counts as the "indexer" as defined in the spec:

> When an initializer target refers to an indexer, the arguments to the indexer shall always be evaluated exactly once. Thus, even if the arguments end up never
> getting used (e.g., because of an empty nested initializer), they are evaluated for their side effects.

The question becomes: is the "indexer" here the virtual `Index`-based indexer, or is it the real `int`-based indexer that is called under the hood? This ends
up having an effect on whether we call `Length` on the collection multiple times or not. While pathological, this _could_ potentially be observable if the nested
member initializer appends to the containing collection in some fashion. If we treat the `Index`-based indexer as the indexer referred to in the spec here, then
appends will be observed. If we instead say the `int`-based indexer is the indexer referred to by the spec, appends would not be observed, as `Length` would only
be evaluated once. This has an even further wrinkle for empty nested initializers: do such initializers actually evaluate `Length`? If the `int`-based indexer is
the "indexer" in the above, the wording would suggest yes, as it is an "argument" to the indexer. However, we also think that there's a line of reasoning where
"argument" is only referring to the user-written code: `Length` is not user written code, so by that logic, it would be safe to elide in the empty case. That leaves
us with a few options:

1. Cache the argument to the virtual `Index` indexer. Revaluate `Length` on every nested member initializer.
2. Cache the argument to the real indexer. Evaluate `Length` even when the nested object initializer is empty.
3. Cache the argument to the real indexer. Do not evaluate `Length` when the nested object initializer is empty.
4. Adopt more aggressive caching across the board.

While we think that, if we were redoing the feature today, we'd pick option 4, we don't think that we can make such a change at this point in the language evolution.
After some discussion, we settled on option 3, falling back to 2 if it ends up being too complicated to implement.

#### Conclusion

Option 3, cache the argument to the real indexer, do not evaluate `Length` when the nested object initializer is empty, falling back to option 2 if it proves an
implementation challenge.
