A call to an instance compound assignment operator becomes "x op= y" or "x++"
only when C# would accept the receiver in that position and bind the same
operator there: the receiver has to be an assignable variable (rather than
"this" in a class, a foreach or using variable, an "in" parameter, or a
readonly field outside its constructor), an operator declared in an interface
needs an interface- or type-parameter-typed receiver, and the operator form
cannot pick an overload by parameter modifier, so a call to an "in" overload
whose by-value sibling is applicable stays a call. A non-public operator has no
operator form at all (CS9308), and stays a call too.
In the other direction, "x = x + y" keeps the explicit spelling wherever
folding it to "x += y" would hand the statement to an applicable instance
operator.
Assisted-by: Claude:claude-opus-5:Claude Code
"x op= y" and a prefix increment resolve in two phases: the instance operators
reachable from the static type of x come first wherever x is a variable -
whether or not the result is used - and the static operators only if none of
them is applicable. A postfix increment whose result is used is the one form
that always binds a static operator. CallBuilder models the first phase when it
checks that recompiling an instance operator call binds the same method; the
fallback phase is deliberately not modeled, since a call the instance
candidates cannot account for must give up the operator form rather than
collide with a static operator.
The same rule cuts the other way for the folds built from static operator
calls: a shadowed fold has to be written in one of the static-binding forms.
The binary operators become "x = x + y"; an increment becomes a postfix
increment whose result goes to a discard, "_ = x++;". That form only exists as
a statement, so the folds that would embed a shadowed increment in an
expression hold back and FixRemainingIncrements gives the increment a statement
of its own instead. A foreach variable cannot be such a receiver at all (it is
read-only), so the loop keeps the existing variable as a writable copy, both
for a plain loop variable and for a deconstruction.
Assisted-by: Claude:claude-opus-5:Claude Code
Record AST transform groups and mutation steps through the C# pipeline, replay selected steps with the stepper, and carry modified-node ranges through output so the Debug Steps pane can highlight the selected mutation without replacing its full step tree.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
The slot kind is now the canonical typed CSharpSlotInfo<T> in Slots, matched by
object identity (the IL SlotInfo model), instead of a parallel SlotKind enum.
CSharpSlotInfo.Kind points at the canonical shared slot; a Slots constant is its
own kind (null Kind, never read -- only per-node slots are asked for their kind),
so there is no self-reference. Child access (GetChild/GetChildren/SetChild,
GetCollectionByKind, AddChild/InsertChild) and the polymorphic
node.Slot.Kind == Slots.X comparisons key on the canonical reference; the
generated SlotKind enum is removed. No behavior change.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Optional single-child slots return T? with a real null instead of a role
null-object, taking the C# grammar as the oracle for which slots are optional.
The generator emits the property type as T? and matches it with MatchOptional,
and consumers move from .IsNull to is null / ?.. This covers the optional
statement, member, try-catch, creation-initializer and pattern slots and the
optional NameSlot tokens. A few slots the grammar marks required but the
decompiler legitimately leaves empty (the implicit-element-access target, an
implicitly-typed lambda parameter's type) are flipped to nullable as well.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Turn on #nullable enable across the AST transform pipeline, ahead of
annotating the slot properties themselves. TransformContext now exposes the
nullable CurrentMember/CurrentTypeDefinition/CurrentModule contract already
declared by ITypeResolveContext, and the generated pattern-to-node conversion
returns a non-null node so patterns can be used in collection initializers
without warnings. No IL changes.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Introduce the successor to node.Role for child-slot identity: the generator
emits a CSharpSlotInfo per [Slot], exposed as node.Slot, plus a shared SlotKind
enum for the polymorphic "is this node in an embedded-statement / condition /
base-type slot?" comparisons a per-node identity cannot express. Migrate the
printer and transform position checks from node.Role to node.Slot and
node.Slot.Kind, and read identifier children and role-keyed writes through the
typed properties. Role is still present and is removed later; output is
unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
For user-defined increments, there were problems with Roslyn was optimizing out one of the stores.
The new transform FixRemainingIncrements now takes increments/decrements that were not detected by TransformAssignment and introduces a temporary variable that can be incremented.
This sometimes requires un-inlining via the new ILInstruction.Extract() operation.
Extract() is not supported in all possible contexts, so it is possible but unlikely that some op_Increment calls remain.
For decimals, the situation is different: legacy csc actually was optimizing "d + 1m" to "op_Increment(d)", so we can get rid of any left-over increments by undoing this optimization. This now happens in ReplaceMethodCallsWithOperators.