I think this isn't reliable enough yet to actually omit parameter types for lambdas (it only protects against switching to the wrong overload; not against type inference failures); so for now it's only used in the query expression transform.
The resolve-result hierarchy was split between ICSharpCode.Decompiler.Semantics
and ICSharpCode.Decompiler.CSharp.Resolver, so consumers had to know which half
a given result came from and import both namespaces. All subclasses now live
next to their base class; MethodListWithDeclaringType follows the method group
it describes, and ILVariableResolveResult gets its own file instead of sitting
among the syntax-tree annotation helpers.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Both types are consumed well outside the C# output layer - DecompileRun
carries the using scope, and the IL transforms build a resolve context from
it - so living in ICSharpCode.Decompiler.CSharp.TypeSystem misrepresented
where they belong and forced a C#-specific namespace import on every
consumer.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Local scopedness is erased from IL and PDBs, so it can only be recovered
from the body. Compare each declaration initializer with later assignments,
field stores, and receiver captures using the C# 11 ref/value escape rules,
and emit scoped only when a later operation is strictly narrower.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
VB emits an auto-property as a "_<PropertyName>" backing field plus accessors
it does not mark [CompilerGenerated]. The pre-C# 14 transform knows this: it
relaxes its accessor requirement whenever it finds such a field, which is how
a VB auto-property still prints as "{ get; set; }".
Routing every property through the field-backed path lost that. Collapsing an
accessor demanded [CompilerGenerated] unconditionally, so a VB auto-property
stopped collapsing and grew explicit "field" accessors instead - correct code,
but noise where every other compiler's equivalent stays a one-liner. Only the
legacy vbc configurations show it, and those run on Windows alone, so the
Linux and macOS jobs stayed green while both Windows ones failed on
VBPropertiesTest and Async.
Assisted-by: Claude:claude-opus-5:Claude Code
Backing-field references inside a property's own get/set/init accessors
are emitted as the `field` keyword at IL-to-AST translation time
(ExpressionBuilder.ConvertField), so arbitrary accessor bodies become
expressible and no separate rewrite pass is needed. Compiler-generated
trivial accessors then collapse individually to `get;`/`set;`, which
also handles mixed shapes like `{ get; set { ... field ... } }`; the
backing-field declaration is removed with its remaining attributes
re-hosted as `field:` sections, and constructor stores become property
initializers (or property assignments, for setter-less properties).
Implicit zero-stores that auto-default struct constructors emit for
unassigned backing fields are dropped rather than lifted.
Recognition stays AST/metadata-based rather than mirroring the ILAst
analysis used for automatic events: events must prove compiler-generated
bodies before discarding them, while the field keyword discards nothing,
so name association plus the accessor context is sufficient.
Below C# 14 (or with the new FieldKeyword setting off), the field
declaration survives under its metadata name, so the UI keeps showing
the truth; EscapeInvalidIdentifiers - the transform the compilable-output
flows (project export, VS, tests) already add - now maps
`<P>k__BackingField` to the readable `P__BackingField` instead of the
generic character escape. A genuine field literally named "field" is
qualified as `this.field` inside accessors, and locals are not named
"field" there, since C# 14 rebinds the bare identifier. Bodiless
accessors mixed into multi-line properties get their own line in the
output.
The fixture covering the feature surface lands with the implementation rather
than as a separate xfailed commit. It is excluded from the test-assembly
compilation because its nullable annotations would trip warnings-as-errors
there.
Assisted-by: Claude:claude-fable-5:Claude Code
Under UseLambdaSyntax, anonymous functions became lambdas only when an
expression body was possible; statement-bodied ones kept C# 2 delegate
syntax. Now every anonymous function whose parameter shape a lambda can
express uses lambda syntax; delegate syntax remains for ref/out/in and
params parameters and for pre-C# 3 language profiles.
Two latent issues surfaced by the wider lambda coverage: DeclareVariables
assumed an insertion point directly under a LambdaExpression is an
expression body it must convert to a block, which block-bodied lambdas
now violate; and anonymous methods declared without a parameter list
carry compiler-generated parameter names like '<p0>' that are not valid
identifiers, so the lambda's mandatory parameter list regenerates such
names from the parameter type: (object obj, EventArgs e) => ...
A side effect visible in fixtures: an explicit parameter list can make
a delegate-creation cast redundant that bare 'delegate' syntax needed
for overload resolution, e.g. new Thread((ThreadStart)delegate { })
becomes new Thread(() => { }).
Assisted-by: Claude:claude-fable-5:Claude Code
A moved field initializer cannot read another instance member. Reject primary-constructor conversion for that case and preserve the original constructor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
A query source can be reached through an indexer as well as through a member
access or a call: `holder?[0].Where(...).Select(...)` puts an IndexerExpression
between the LINQ call and the `?.`. The receiver walk stopped there, so query
syntax was still introduced over a source the conditional access had lifted to
a nullable value type, and the output failed to compile with CS1936 - the same
way as the case that was reported, one node kind further along.
IndexerExpression.Target is nullable where MemberReferenceExpression's and
InvocationExpression's are not, so only that arm needs to match on the target.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Query syntax cannot preserve a null-conditional receiver that lifts a value type. Detect null conditionals through the LINQ receiver chain before introducing query syntax.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
A C# anonymous type is immutable and compares every member. VB's are neither
unless every property is declared 'Key': otherwise the properties are settable
and only the 'Key' ones take part in Equals and GetHashCode. Writing such a
type as 'new { ... }' silently gave it value equality and made any assignment
to one of its properties fail to compile, so only an anonymous type with no
settable property is treated as one; the rest keep their own declaration.
Those declarations carry the shape VB gave them, so the round-trip preserves
both mutability and 'Key' equality. Their names are the remaining obstacle,
since the VB compiler separates the parts with '$': the type, its backing
fields and any local named after it are renamed to use '_' instead, and a
comment on the declaration says why the type is spelled out.
Generated variable names are now rejected when they would not be legal C#
identifiers, which also stops a display class from lending its unspeakable
name to a local in the NoLocalFunctions output.
Assisted-by: Claude:claude-fable-5:Claude Code
Operator precedence made the condition read
(setting && Add) || Subtract, so 'x -= 1' was converted to 'x--' even
with IntroduceIncrementAndDecrement disabled. Only observable with the
non-default setting, which no fixture configuration exercises, so no
test accompanies the fix.
Assisted-by: Claude:claude-fable-5:Claude Code
Reviving the 2020 test-cases-fp-types fixtures (compound assignment on
float, double and decimal) exposed an asymmetry: post-increment and
post-decrement on float/double round-tripped as x++/x--, but the pre
forms came back as x += 1f because the increment detection in
PrettifyAssignments only accepted integer constants. C# defines ++/--
on floating-point types as adding or subtracting exactly 1, so the
conversion is exact for a constant 1 operand. Decimal already works
through the op_Increment/op_Decrement path.
Assisted-by: Claude:claude-fable-5:Claude Code
The compiler-generated documentation file contains a P: entry for a
parameterized property, but its accessors are emitted as ordinary
methods, whose M: id has no documentation entry. Fall back to the
owning property's documentation both when inserting XML documentation
into decompiled output (on the first accessor only, to avoid
duplicating it) and in the text view's tooltip (for either accessor).
Assisted-by: Claude:claude-fable-5:Claude Code
Object creation can require an unsafe context solely because the selected
constructor has a pointer parameter. Apply the existing unsafe-signature
check to object creation nodes so the emitted declaration remains compilable.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
With recognition on the ILAst and reference substitution during
translation, the four accessor-body patterns in
PatternStatementTransform were only reachable as a fallback, and any
divergence between them and the AutoEventDecompiler verdict produced
inconsistent output. A compiler shape the ILAst matchers do not know now
degrades to explicit accessors with the backing field kept in the
output, which stays compilable. Bodyless events (abstract, extern,
interface members) previously relied on the patterns' no-body clause to
become field-like; since C# cannot express bodyless custom accessors,
DoDecompile now chooses the field-like form for them directly. Also
deletes the orphaned IsEventBackingFieldName helper; the name
association lives in PropertyAndEventBackingFieldLookup.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
PatternStatementTransform renamed backing-field identifiers to the event
after the fact, keyed on the metadata name association alone: references
belonging to an event that is not actually automatic were still renamed,
binding them to a custom event that is unusable as a value (the same
defect class as #3858), and the rename bypassed the resolver checks, so
qualifiers were computed for the hidden field instead of the printed
event. ExpressionBuilder.ConvertField now performs the substitution,
keyed on the AutoEventDecompiler verdict whose memo moves into
DecompileRun so that member hiding, the event declaration, and reference
translation all decide from one analysis. Checking the verdict's field
identity also keeps same-typed sibling events apart (#3575), and the
qualifier logic running against the event drops spurious this./type
qualifiers from raise sites.
mcs 2.x accesses a sibling automatic event's backing field directly
inside custom accessors instead of calling the accessor, so the fixture
expects the resulting Delegate.Combine form there.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
The syntactic accessor-body patterns in PatternStatementTransform sit
downstream of every settings-dependent transform, so each new compiler
shape or settings combination silently broke recognition: with
AggressiveInlining enabled, static events inline the Delegate.Combine
call into CompareExchange positionally, which none of the four patterns
matched, while call sites were still rewritten to the event name from
metadata alone - producing uncompilable output (CS0079).
Recognition now happens in DoDecompile(IEvent) by structurally matching
the ILAst of the accessors, decompiled with a fixed set of settings the
same way RecordDecompiler analyzes method bodies. This makes detection
independent of the user-visible settings by construction. Events that
are not recognized fall back to the classic path unchanged, including
the existing AST patterns.
mcs 2.x compiles the accessors as a compound assignment, evaluating
'this' once via IL 'dup'; the simple-combine matcher accepts that
stack-slot alias.
Assisted-by: Claude:claude-fable-5:Claude Code
Passing the same local as multiple out arguments of one call made
DeclareVariables turn the first use into an implicitly-typed declaration,
producing 'f(out var x, out x)'. Referencing an implicitly-typed out
variable in another argument of the declaring call is rejected by the
compiler (CS8196), because its type is only inferred once overload
resolution of that call has completed. The explicitly-typed form
'f(out int x, out x)' is valid, so fall back to the explicit type in
that case.
Assisted-by: Claude:claude-fable-5:Claude Code
Backfills the standard MIT X11 header on hand-written files that never
got one, attributing each to its first-commit author and year from git
history. Code vendored from dotnet/runtime and Humanizr/Humanizer gets
its origin's license lines and a provenance note instead. Generated
files (Resources.Designer.cs, the version-info template), tool-managed
suppression files, and BAML test-case fixtures intentionally stay
header-less.
Assisted-by: Claude:claude-fable-5:Claude Code
The C# compiler lowers a foreach over an inline array into a for loop whose
body reads each element through <PrivateImplementationDetails>.InlineArrayElementRef(ref
buffer, i). That helper cannot be named in C#, so the decompiled for loop did
not compile.
Reconstruct the foreach at the AST level instead. Rewriting the unchecked
InlineArrayElementRef helper to the bounds-checked indexer buffer[i] would be
unsound for an out-of-bounds index, so the transform fires only when the loop
bound equals the inline array's length: that proves 0 <= i < length, matching
the exact shape the compiler emits and nothing else. A loop that does not match
keeps the faithful (if unnameable) helper call rather than silently gaining a
bounds check.
Assisted-by: Claude:claude-fable-5:Claude Code
Per @siegfriedpammer: rather than rescue the hoisted for-initializer in
DeclareVariables, don't form the for-loop at all. TransformFor now bails when the
loop variable is a by-ref-like local used after the loop, leaving the while-loop
(matching source); the ref decl keeps its initializer, no CS8174. Reverts the
DeclareVariables change; test now expects the while form.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
A ref local that is used after a for-loop has its declaration hoisted in front of
the loop, but its only initialization is the for-initializer ref-assignment. The
declaration was then emitted without an initializer (`ref T x;`), which does not
compile (CS8174).
When a by-ref-like local's matching assignment is the first for-initializer, move
the ref-assignment's value up into the declaration (`ref T x = ref expr;`) and
drop the for-initializer.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
A type's leading field assignments are extracted to field declarations
only when every constructor that does not chain with this() agrees on
them. When they disagree, the analyzer gave up on the whole type, so the
remaining constructors' this()/base() calls were never lifted into
initializers -- a struct with two divergent constructors plus a chaining
one rendered the chain as `this = new TSelf(...)` instead of `: this(...)`.
Chain lifting does not depend on the shared-initializer extraction, so a
mismatch now just skips the extraction (the assignments stay in the
bodies) and the transform continues. Primary constructors still bail,
since their parameters drive the initializers that must be extracted.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The step recorder is shared by C# AST and ILAst replay, so keep the public infrastructure out of IL.Transforms and pass language-specific node navigation through neutral node metadata.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
The IL (Stepper) and C# (TransformContext) paths each recorded the changed
node, its seam neighbours, and its ancestor chain with duplicated logic that
could drift. Move the ordering/dedup/seam strategy onto
Stepper.Node.RecordModifiedNode; each language keeps only its own node
navigation.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Keep the IsUnsafe visitor path context-free while avoiding repeated null guards around debug-step recording in the unsafe modifier transform.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Keep existing modified-node candidates when a transform cannot provide a produced node, matching the IL transform helper and preserving highlight fallback quality.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
When the step limit falls on a C# transform step, Stepper.Step records the node
as LimitReachedStep but throws before TransformContext can attach the node's
highlight candidates, so the 'show state before' view had only the bare modified
node to resolve against -- and nothing if that node renders no text of its own.
The IL path already records its candidates before the throw; mirror that on the
C# side by attaching the candidates to the limit-reached node in the catch, then
re-throwing so the pipeline still halts.
Assisted-by: Claude:claude-opus-4-8:Claude Code
NodeLookup.AddNode indexed every annotation of every rendered node by reference
identity, but the debug-step highlighter only ever looks up the DebugStepMarker;
the rest were dead keys, and a shared annotation (ResolveResult and friends,
copied across nodes) would resolve to whichever node rendered last. Make
DebugStepMarker public and bridge only it -- behaviour-preserving for resolution
while dropping the per-annotation dictionary churn on every rendered node.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A step that removes a node has nothing left to highlight in the resulting text,
so range resolution fell back to the enclosing block and flooded it. Record the
changed node's surviving neighbours as seam anchors (captured before the
mutation) and split a step's candidates into precise / seam / ancestor tiers:
when neither the node nor its marker resolves, place a zero-length caret at the
gap -- the successor's start, else the predecessor's end -- and only fall back
to the enclosing block when no neighbour survives. A zero-length highlight is
rendered as a caret (positioned, pulsed, centered) with no background mark.
Assisted-by: Claude:claude-opus-4-8: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
A field, auto-property, or event initializer is written once at its
declaration, but in IL it runs in every instance constructor that does not
chain to this(...) (and static initializers run in the static constructor).
The decompiler lifts the initializer from a single constructor, so its
breakpoint was emitted only there and the other constructors had none.
Two causes are addressed:
- The lift discarded the initializer's copies in the other constructors.
They are now kept on MemberInitializerInOtherConstructorsAnnotation and
replayed by SequencePointBuilder, mapping the same source location onto
each constructor's IL.
- PortablePdbWriter only emitted methods that DebugInfoGenerator discovered
through declaration syntax, so a constructor whose declaration is omitted
from the output (implicit default ctor, implicit static ctor, primary
ctor) dropped its generated points. Those functions are now emitted by
walking the sequence-point map directly.
PdbGen fixtures cover single, multiple, this()-chained, implicit, static,
primary-constructor, and field-like event initializers, pinning the
reconstructed breakpoint map against the C# compiler's.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A slot kind names one child position, so the generator emits a single
typed Slots.X constant for it. A kind used with two different child types
had to widen that constant to AstNode, and the typed GetChildren<T>
accessor then cast the live AstNodeCollection<Concrete> to
AstNodeCollection<AstNode> -- an InvalidCastException waiting for the
first caller (latent today, but the model permitted it).
Make the loose state unrepresentable instead of guarding it at runtime:
DSTG001 errors when a [Slot] kind is declared with more than one child
type. The six kinds that only coincidentally shared a name (Body, True,
False, Initializer, SwitchSection, Variable) are split into precisely
typed kinds; a genuinely either/or position (a lambda body) stays one
declared type, AstNode. Every Slots.X now carries its real element type,
so the GetChildren cast can no longer fail.
Output is unchanged: the Pretty suite stays byte-identical with
CheckInvariant green in DEBUG.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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
The generator emits a Slots holder of typed CSharpSlotInfo<T> kind constants (T is
the child type, widened to AstNode for the few kinds reused with several child
types). Call sites whose kind maps to a single type switch from
GetChildByRole<T>(SlotKind.X) to GetChild(Slots.X), inferring the result type from
the slot. The ByRole methods and SlotKind remain for the multi-type Attribute
accessor and the internal add/insert plumbing, removed in following steps.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Completes the nullable migration for ICSharpCode.Decompiler/CSharp/Syntax: every
hand-written file now carries #nullable enable. The pattern-matching API
(INode/Pattern/PatternExtensions DoMatch/Match/IsMatch) takes a nullable 'other',
since matching legitimately compares a pattern against a missing child. Members
that genuinely return or accept null are annotated to match (AttributeSection's
target token, IAnnotatable.Annotation<T>, Statement/Expression.ReplaceWith,
SyntaxExtensions.GetNextStatement), and a latent null dereference in
Backreference.DoMatch is fixed.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The C# syntax tree's name accessors (ParameterDeclaration.Name, GotoStatement.Label,
catch variable, query continuations, ...) are convenience strings over a backing Identifier
token. Names that may be absent are now typed string? and read as null when absent, instead of
a non-null empty-string sentinel; required names stay non-null string. The backing token slot's
nullability follows, and an empty or null assignment clears the token (empty == absent).
Because the property type now carries optionality, the separate [NameSlot] attribute and its
nullOnEmpty flag are redundant: a [Slot] on a string property is a name (child slots are
AstNode-typed, so the type disambiguates), and the generator infers optionality from the
declared nullable annotation -- which a string? declaration already requires #nullable for. The
repeated empty-to-null setter body becomes Identifier.CreateIfNotEmpty. To make the annotation
readable, #nullable enable is turned on across the syntax node files (the directive the inferred
optionality depends on), with the attendant local nullability fixups. Consumers that feed a name
into a non-null slot assert it where the variable structurally has a name.
Assisted-by: Claude:claude-opus-4-8:Claude Code
GetChildByRole<T> returned default! while it can legitimately yield null (empty slot
or a kind the node does not declare); it now returns T?. Callers that already null-checked
are unchanged; the few that rely on a structural guarantee (name tokens, label identifier,
property accessors) now state it with an explicit !, and MethodDeclaration drops a redundant
cast that the lie required.
Also removes two members left dead by the Role removal: the AddAnnotation override that
only forwarded to the base, and GetCSharpNodeBefore, which ignored 'this' and had no callers.
On the output path, GetChildNodes() allocates a snapshot List per call; the read-only
.Children traversals that never mutate the tree now walk the live FirstChild/NextSibling chain
(O(n), allocation-free, since child indices are cached), an EmptyStatement child check uses
HasChildren, and the label-statement sibling scan breaks on the first match instead of walking
to the end of the block. The generated GetChildNodes snapshot is kept where traversal may mutate.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Turn on #nullable enable across the AST consumer layer: the output visitor, the
IL-to-C# builders (statement, call and expression builders, CSharpDecompiler,
TypeSystemAstBuilder), the translation-result wrappers, the sequence-point and
required-namespace collectors, and the annotation helpers. Optional inputs,
fields and returns are typed nullable, detector out-parameters use
[NotNullWhen(true)], and structurally-guaranteed dereferences use the
null-forgiving operator. A few public parameters that already tolerate null are
widened to match their downstream callers. The annotations emit no IL, so the
Pretty suite stays byte-identical.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Each concrete node's constructors are now emitted by the source generator
from its members in source order: single and collection [Slot] children,
the [NameSlot] string, and settable enum scalars (Operator, FieldDirection,
...). It emits the empty ctor (for object initializers), a required-prefix
ctor, one ending at each collection, and the full ctor, with later ctors
forwarding to shorter ones via this(...) and a params[] overload alongside
each IEnumerable<T> one. The hand-written ctors the generator now produces
are removed; scalar/location/Identifier convenience overloads that it cannot
express are kept (e.g. AssignmentExpression(left, right), SimpleType(Identifier),
the string+TextLocation overloads).
Because ctor parameters follow source order, BinaryOperatorExpression and
AssignmentExpression declare Operator between Left and Right so the generated
ctor is the expected (left, op, right). Pure-scalar nodes whose state is not
in slots (e.g. PrimitiveExpression's literal value) are left untouched.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Slot identity already lived in node.Slot/CSharpSlotInfo/SlotKind after
the storage flip, so the parallel Role/Role<T> child model was redundant.
The Role-keyed mutation/query API is reexpressed over SlotKind, and the
role-index packing on AstNode flags is gone.
Because a node's Slot is now derived from its index in its parent rather
than stored on the child, the located-AST reattach in
InsertMissingTokensDecorator must capture the child's slot kind before
Remove() detaches it.
Assisted-by: Claude:claude-opus-4-8:Claude Code
With every optional slot nullable, the null-object pattern is dead. Generated
non-nullable getters return the backing field directly, which surfaced a last
tier of slots the decompiler legitimately leaves empty (omitted range operands,
an implicitly-typed array creation, unnamed parameters, an unbound generic
argument, and others) and flips them to nullable too. The machinery is then
removed entirely: the per-node null classes, the .Null statics and
VisitNullNode, AstNode.IsNull, the role null object, and Identifier.Null.
AcceptVisitor becomes unconditionally generated, and consumers move from
.IsNull to is null and from unconditional visits to ?.AcceptVisitor.
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
Children were kept in a per-node doubly-linked list with the slot accessors
layered over it as a view. Storage now is the slot model: each node stores its
children in generated backing fields, AstNodeCollection<T> is backed by a
List<T>, and the flattened child-index space is owned by generated
GetChildCount/GetChild/SetChild/GetChildSlot members, with sibling navigation,
the role API and Clone re-expressed over them and indices renumbered lazily. A
DEBUG CheckInvariant runs after each transform, the analog of the IL
pipeline's per-transform check, so a transform that corrupts the tree fails at
that transform. Output is unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Comments and preprocessor directives were positional children interleaved
into the child list, and punctuation, keywords and operators were token-node
children. Add a leading/trailing trivia side-channel for comments and
directives, emit it from the output visitor, and re-home every comment
receiver onto it (including inside-block comments as comment-only empty
statements and undecodable attribute arguments as an ErrorExpression). With
locations and sequence points no longer sourced from token nodes, stop
reconstructing them on the locations path and delete CSharpTokenNode,
CSharpModifierToken and InsertSpecialsDecorator. The AST no longer carries
token children or positional comments; output is byte-identical.
Assisted-by: Claude:claude-opus-4-8:Claude Code