Nodes use nullable reference types now, so the generated null-object
node and its hasNullNode / NeedsNullNode / NullNodeBaseCtorParamCount
plumbing are dead. Drop them and reword the affected comments.
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
The 124 concrete node classes that are neither a base of another node nor a
host for a generated PatternPlaceholder subclass are now sealed, so the JIT can
devirtualize the slot dispatch (GetChild, GetChildCount, GetChildSlotInfo,
GetChildNodes, AcceptVisitor) when the static type is the sealed leaf. Identifier's
constructor becomes private (protected is meaningless and an error in a sealed
type). Abstract bases, base classes (PreProcessorDirective, the EntityDeclaration/
AstType/Statement/Expression hierarchy roots), and pattern-placeholder hosts stay
open.
Assisted-by: Claude:claude-opus-4-8:Claude Code
AstNode.Children and the visitors' VisitChildren walked children via
FirstChild/NextSibling, an O(slots) index rescan per step. The generator now
emits GetChildNodes, which materializes the children in slot order in one pass
(O(children)); Children and VisitChildren iterate that. The snapshot makes the
walk tolerant of the visitor removing or replacing the current child (the
mutation pattern the old capture-next loop supported) without re-feeding the
loop.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The [Slot] and [NameSlot] string argument only feeds SlotKind derivation, which
takes the last dotted segment and strips a trailing "Role", so the Roles.
qualifier and the Role suffix were dead decoration. Rewrite each to the bare
SlotKind it already produces, so the attribute reads as exactly the kind.
Collapse the runs of consecutive blank lines that removing the hand-written
constructors, fields and properties left between the remaining members.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The decompiler never emits "delegate () {}" (an explicit empty parameter
list) -- it produces "delegate {}" when there are no parameters and
"delegate (...) {}" when there are -- so HasParameterList always equalled
Parameters.Any(). Drop the property, its backing field and the two setter
calls, and read Parameters.Any() directly at the printer and the
resolve-result builder.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Scalar value properties that merely forwarded to a private backing field
(IsAsync, Operator and the operator-type enums, Format, ClassType, Variance,
ParameterModifier, the parameter bool flags, and similar) become
auto-properties, formatted on a single line. Behavior is identical;
PrimitiveExpression's constructor sets the property instead of the dropped
field.
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
TokenRole was a printer-side descriptor whose only jobs were holding a token's
text and giving the writers an identity to single out specific tokens. The text
becomes plain const strings on the nodes, and the few identity checks are
reexpressed as node-stack context: interpolation braces are recognized by an
Interpolation on the writer's stack, record class versus struct coloring keys
off TypeDeclaration.ClassType, and the accessor/this/base/override cases fall
out of the surrounding node. WriteKeyword/WriteToken drop the descriptor
parameter. The constants are named for what the token is: a keyword, a symbol
token, or a modifier.
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
A [NameSlot("role")] partial string property makes the generator own the
backing Identifier token slot, the string accessor, and the match term, so a
convenience name string and its hand-written token slot collapse to a single
declaration. A nullOnEmpty option stores a null token for an empty name, used
where the output visitor keys off an absent token. Apply it across every
name-token node.
Assisted-by: Claude:claude-opus-4-8:Claude Code
NodeType was NRefactory's coarse node category, but only three reads remained
here: two checks now expressed as "is not Trivia" and one debug assert for the
pattern category. Remove the enum, the abstract property, every per-node
override, and the generator's emission, preserving the pattern-placeholder case
through an IPatternPlaceholder marker interface the output-visitor assert
checks. Also remove the unused PrimitiveExpression.AdvanceLocation helper.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Delete source-fidelity state the decompiler never reads, since it generates
the AST and never parses: SyntaxTree.FileName/ConditionalSymbols/TopExpression,
Comment.StartsLine/IsDocumentation, the preprocessor line/file fields, and a
computed ComposedType flag. Hoist the duplicated start/end location storage
into the shared Trivia base, and drop the redundant per-instance Location field
on single-token leaf nodes in favor of the base's print-time location.
Generate DoMatch for the last nodes (Comment, SyntaxTree, the using and
namespace declarations, Identifier).
Assisted-by: Claude:claude-opus-4-8:Claude Code
Generate DoMatch across the remaining expression, statement, type-member,
type-reference and general-scope nodes, including the inherited
EntityDeclaration name/return-type/attribute match, and stop generating it for
abstract base nodes. Matching every structural member fixes real
under-matching bugs (PointerReferenceExpression ignored its Target;
ExtensionDeclaration matched anything) while computed or derived members are
excluded from matching. PrimitiveExpression and PreProcessorDirective stay
hand-written by design. Pretty output is byte-identical.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The grammar-production doc comments were transcribed in ANTLR style and
often copied the C# spec verbatim, including sub-rules the AST does not
model. Rewrite them as W3C EBNF (::=, ?, *, +), unroll sub-productions
that are not themselves AST nodes (e.g. anonymous_function_modifier ->
'async'), and shape each production to the node's actual members. Render
top-level alternations of node productions as multi-line <code> blocks;
keep operator and keyword token lists inline.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Replace the hand-written DoMatch overrides on the expression nodes with
generated ones. The generator matches every real child and structural member,
so several matchers become stricter than the hand-written versions that
under-matched (for example a previously-skipped child or an ignored flag),
while Pretty output is unaffected. Convenience-string identifier slots are
excluded so the name string is matched once rather than also via its token.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Preparation for making optional single-value slots nullable: a role may omit
its null object and a slot may be cleared via SetChildByRole(role, null), and
the generated DoMatch emits MatchOptional for a nullable single child so an
absent child matches correctly. Both are inert until a slot is actually made
nullable.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Each concrete syntax node now carries, in an XML-doc <remarks> block, the
matching production from the C# language specification grammar (ECMA/Microsoft,
ANTLR notation), quoted verbatim. Aggregate nodes (e.g. BinaryOperatorExpression,
ComposedType, TypeDeclaration) list every production they span; lexical/trivia
nodes (Comment, the preprocessor directives, Identifier) cite the lexical rule.
Nodes with no spec production -- ErrorExpression, UndocumentedExpression,
InvocationAstType, TypeReferenceExpression, NamedExpression, DocumentationReference,
and the C# 14 ExtensionDeclaration -- carry a hand-written EBNF plus a note
explaining why no official production exists.
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
The token writers and the UI syntax highlighter only need a token's identity (to
single out structural braces, the constructor this/base keyword, the override
modifier, and to colour keywords) -- not the AST child-role machinery. Make
TokenRole a standalone printer-side descriptor instead of a Role, turn the
modifier marker into a TokenRole, and change the WriteKeyword/WriteToken
signatures from Role to TokenRole across the writer hierarchy and the highlighter.
This lets the child Role hierarchy be removed without disturbing token output.
The dead OptionalComma/OptionalSemicolon bodies (no comma/semicolon/whitespace
children exist since the token drop) become no-ops, and the few sites that handed
the writer a child role for a keyword now pass none. Output is unchanged across
the decompiler suite, and the UI builds.
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
The pattern matcher walked collections through INode.Role/FirstChild/
NextSibling, skipping siblings of a different role. Now that each
AstNodeCollection<T> is already the per-role child list, the engine matches two
collections by list index, and INode sheds Role/FirstChild/NextSibling
entirely. A collection exposes its IReadOnlyList<INode> view through a cached
adapter rather than implementing the interface directly, so a typed collection
does not become ambiguous for LINQ. Characterization tests pin the matcher's
behavior first.
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
Source locations were virtual, computed by recursing to the first and last
child, whose leftmost and rightmost leaves are token nodes; sequence-point
coordinates likewise came from reconstructed token nodes. Store locations as
fields assigned while printing, and derive sequence-point coordinates from the
surrounding real nodes plus the decompiler's fixed formatting, so neither
depends on token children. The using/foreach await modifier becomes a plain
bool field. Characterization gates lock the emitted locations and PDB
coordinates, which are unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Member and local modifiers were stored as modifier token children, and a
ComposedType's ref/readonly/nullable/pointer specifiers and an array rank as
token and comma children. The output visitor already derived all of these
from scalar accessors, so move them to plain enum/bool/int fields. This
removes another dependency on token children ahead of deleting the token
nodes; the emitted keyword and specifier sequences are unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The InsertMissingTokensDecorator path (TokenWriter.CreateWriterThatSetsLocationsInAST)
reconstructs token nodes and assigns source locations onto the AST, feeding PDB
sequence points and GUI navigation. The Pretty suite never drives it, so it had no
coverage at all. Before reworking the token model, lock its observable consequences:
the located path emits the same text as the plain path, real nodes receive ordered
locations, location-based navigation resolves into the method body, and sequence
points are produced for a method body.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Each child of a C# AST node is declared as a [Slot] partial property, and the
source generator emits the accessor bodies and an ordered slot schema
(SlotCount/GetSlotRole/IsCollectionSlot) from them. Generating the schema
keeps slot order from being mis-stated by hand and lets a DEBUG invariant
check declared slot order against document order on every decompile. The node
hierarchy is converted family by family; the EntityDeclaration leaves flatten
their inherited Attributes/ReturnType/NameToken into each leaf's ordered slot
set. Storage stays the NRefactory linked list at this stage, so only the
declaration model changes and output is unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The generator emits the IAstVisitor interface, the AcceptVisitor overloads,
and the null-node and pattern-placeholder nodes from [DecompilerAstNode]
declarations, so drop the hand-written equivalents across the C# AST: per-node
AcceptVisitor/DoMatch, the #region Null / #region PatternPlaceholder blocks,
IAstVisitor.cs, and now-dead usings. Also adds AccessorKind and moves
IdentifierExpressionBackreference into the PatternMatching folder.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Introduce a Roslyn source generator that emits the visitor boilerplate for
the C# AST from [DecompilerAstNode]-tagged node declarations: the
IAstVisitor interface, the AcceptVisitor overloads, the pattern-placeholder
nodes, and the initial DoMatch support. AccessorKind lets an accessor's
keyword be chosen independently of its role, an early step toward shedding
the NRefactory role model.
The C# AST inherited NRefactory's freezable model (IFreezable, Freeze,
IsFrozen, a frozen flag bit, and ThrowIfFrozen guards on every mutator),
but the decompiler never uses it: nothing calls Freeze(), not even the
generated null-node singletons, so every IsFrozen guard only ever
evaluated false. The decompiler is single-threaded and never shares or
freezes nodes. Remove the whole apparatus as preparation for the
slot-based AST rewrite, which has no place for it. Roles are untouched
here, so the flags word keeps its role index; only the freed frozen bit
goes away.
--list-* printed generic types without their `n arity suffix, yet -t only
accepted the exact reflection name, so a name copied straight from the listing
threw "Could not find type definition". The listing now prints the reflection
name, and -t resolves through a ladder of progressively looser, uniqueness-
checked rules: the engine's exact lookup, an arity- and nesting-separator-
insensitive FullName match, a case-insensitive match, a namespace-less simple
name, and a trailing segment path ("Dictionary.KeyCollection"). The input is
parsed with System.Reflection.Metadata.TypeName and reduced to its underlying
definition first, so assembly qualification, generic arguments, and
array/pointer/byref decorations cannot corrupt the comparison key. An ambiguous
name reports its candidates instead of guessing, and a miss prints name
suggestions and returns EX_DATAERR rather than dumping a stack trace.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Clicking a reference left AvaloniaEdit's selection-drag mode stuck, so a
subsequent mouse move (with no button held) extended a selection.
AvaloniaEdit's SelectionMouseHandler captures the pointer and enters
selection mode on press, and extends the selection on every move while that
mode is active -- it keys off the mode, not whether a button is down. Only
its bubble-phase PointerReleased handler resets the mode and releases the
capture, and that handler early-returns when the event is already handled.
The reference-click handler ran in the tunnel phase and marked the release
handled before AvaloniaEdit saw it, so AvaloniaEdit's cleanup never ran.
Move the release handler to the bubble phase so it runs after AvaloniaEdit
has reset its state. AvaloniaEdit subscribes from the TextArea constructor,
before this view attaches its handlers, so the ordering is deterministic and
navigation can stay synchronous.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The "IL with C#" view decompiles each method body as a bare handle, so a
static constructor is decompiled without its type's field declarations in
the syntax tree. MoveFieldInitializersToDeclarations then could not find a
declaration to move the static-field-initializer statement onto, asserted
(kind was Static, not Primary) and dropped the statement -- crashing Debug
builds and silently losing the assignment in Release.
Dropping the statement is only correct for the primary-constructor case,
where the assignment's backing member is synthesized and has no separate
declaration. For static/instance initializers a missing declaration just
means the member is not part of this partial syntax tree, so the
assignment must remain in the constructor body.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Navigating to a target on startup first eagerly loads every relevant
assembly's metadata so the entity search that follows can resolve it.
That pre-load used the throwing GetMetadataFileAsync, so a restored
session that still referenced an assembly whose file had since been
deleted or moved crashed startup with an unhandled
DirectoryNotFoundException instead of simply skipping the gone entry.
Use GetMetadataFileOrNullAsync there: a missing or unreadable assembly
now resolves to null and is skipped, which the entity search already
tolerates (it uses the OrNull variant too).
Assisted-by: Claude:claude-opus-4-8:Claude Code
Runtime async is a compiler feature that emits ordinary async/await (a
C# 5 construct), so reconstructing it should not require selecting C# 15.
The dedicated RuntimeAsync setting was also redundant: AsyncAwaitDecompiler
already runs the runtime-async transforms only when AsyncAwait is enabled.
Fold the behavior into the AsyncAwait setting and drop the separate toggle.
Assisted-by: Claude:claude-opus-4-8:Claude Code
When a breadcrumb path was longer than the bar, the Simple theme's horizontal
scrollbar appeared in a reserved layout row inside the 28px bar: it shifted the
crumbs up when it showed and covered more than half the bar's height.
Three ways to handle the overflow were considered:
1. Collapse the head into an "..." overflow dropdown (leading crumbs fold into
a left button; the deepest crumbs stay visible) - the Windows Explorer / VS
nav-bar / Files behaviour.
2. Hide the scrollbar entirely and scroll the trail with the mouse wheel,
keeping the current-node end visible.
3. [CHOSEN] A thin, auto-hiding overlay scrollbar that draws over the content
(reserves no height, so nothing shifts) and fades in only when the path
overflows and the pointer is over the bar.
Avalonia's Simple ScrollViewer reserves an Auto grid row for the horizontal
scrollbar and its ScrollBar has no auto-hide, so the overlay is built here: the
breadcrumb's built-in bar is hidden (no reserved row, no shift) and a thin,
button-less ScrollBar is layered at the bottom, bound to the viewer's Offset,
ScrollBarMaximum, and Viewport via small Vector/Size converters. It fades in on
pointer-over and the mouse wheel scrolls the trail horizontally.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ILSpy showed the current location only implicitly in the tree selection and kept search in a separate docked pane. The omnibar, modelled on the Files community app and jiripolasek's EditorBar, puts an address-bar atop each decompiler document: a breadcrumb of the node (Assembly > Namespace > Type > Member) whose segments navigate and whose chevrons list child nodes, turning into a search box on typing that reuses the existing search engine. It coexists with the docked search pane and ships off by default behind the Options / Display 'Tab options' EnableOmnibar toggle, which applies live.
Assisted-by: Claude:claude-opus-4-8:Claude Code