An argument that repeats its parameter's default value may be left out,
but the value was only ever compared against the method the call
instruction names - for a virtual call the base declaration, since that
is the slot the compiler emits. The shortened form binds against the
receiver's static type, where an override is free to declare a different
default, and the recompiled code then passes that one instead. Calls have
had this since optional arguments were introduced; opening indexer
accesses to omission brought it to element accesses too.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
HandleAccessorCall had no way to express an omitted argument, so
CallBuilder asserted that none had been detected before it got there: any
assembly indexing through an indexer with an optional parameter hit that
assert in a Debug build, and Release wrote the defaults back out. Accessor
calls now go through the same ArgumentList helpers as an ordinary call.
Two things had to reach them. The assigned value of a setter is the last
argument of the accessor call but not an argument of the access - the
standard adds it only for the invocation (12.6.2.1) - so it neither ends
the run of optional arguments nor is written out with them, and whether an
accessor is written as an access at all is decided once, before the
arguments are translated, so the scan and the count cannot disagree. The
names have to stop where the argument list does, and they name the
indexer's parameters, which the type system takes from the getter rather
than from the accessor being called.
C# allows named arguments in an element access, but NamedArgumentTransform
refused to introduce one for any accessor, so an access whose arguments
the compiler reordered came out as a temporary. Only indexers gain this:
a property access has no argument list, an operator cannot take names,
and a setter's value stays unnamed on the right-hand side. Introducing a
name replaces the call with a block, so it is refused where the
surrounding instruction requires the call itself - a call-inline-assign
block, or the target of a compound assignment.
Whether the shortened access still binds to the same member is left to
IsUnambiguousAccess. If it does not, the omitted arguments are written out
again before any cast is tried, since restoring them cannot change what
the access means. A type declaring both this[int] and this[int, int = 10]
therefore keeps both arguments; the fixture pins that.
Not covered: params indexers, [Optional] without a constant,
[DateTimeConstant]-style defaults, default(T) at a value-type
instantiation, and omitting a middle optional argument - the last of which
plain calls do not do either.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Shortening default(T) is the same problem as removing the redundant cast
around a lambda whose delegate type the context already fixes, so it uses
the same mechanism: ConvertTo makes the explicit type implicit when the
conversion is an identity conversion and the caller allows an implicit
one. The literal keeps the type it was shortened from, so any later
conversion to a different type - or any context that requires an explicit
type, such as an overload resolution recheck falling back to CastArguments
- can spell default(T) out again. That keeps the value intact where the
bare literal would change it, e.g. "object o = default(SomeStruct)", which
boxes a non-null struct while "default" would be null.
Because the shortened literal resolves to DefaultLiteralResolveResult,
CallBuilder's existing overload resolution recheck sees a real default
literal and rejects ambiguous calls on its own; no separate bookkeeping
about which arguments may stay untyped is needed. Only the contexts that
supply no target type at all restore the explicit form: an awaited
expression, and arguments of operator methods, which later become operator
or cast syntax rather than calls.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code