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
OnSelectionChanged deferred its Steps = null via Dispatcher.Post. The selection
message is raised synchronously on the UI thread right as the new selection's
decompile is kicked off (AssemblyTreeModel.RaiseSelectionChanged), so a deferred
clear can land on a later dispatcher cycle than the decompile's StepperUpdated
populate post and wipe the freshly recorded steps -- leaving the pane empty
until the next decompile (the intermittent CI timeout in the debug-step UI
tests, where both C# and ILAst share this handler). Clear synchronously so the
blank is pinned to the selection moment, strictly before that selection's
decompile can finish and post its steps.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The replay tests only checked DebugStepHighlight was non-null, which the
ancestor fallback satisfies unconditionally -- a regression widening every
highlight to the enclosing method would have passed. Assert instead that the
range lies in bounds, does not span the whole document, and (unless it is a
zero-length removal caret) covers non-whitespace rendered code.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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
The debug-step node bracket (MarkNodeStart/try/finally/MarkNodeEnd) was copied
into every hand-written WriteTo override and re-emitted by the T4 generator, so
a newly added instruction could silently omit it and lose step highlighting with
no compile error or test failure. Seal WriteTo on ILInstruction to apply the
bracket once and delegate to a new abstract WriteToCore; move every override
(hand-written and generated) to WriteToCore without the wrapper. Rendered output
is unchanged -- the marks are no-ops unless the output tracks nodes.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The step tree can run to hundreds of entries per decompile, so finding a
specific mutation means scrolling. Add a filter box in the pane's top-right
corner: a row survives when its description -- or any descendant's -- contains
the text (case-insensitive), keeping the path to every match, and the tree
auto-expands while filtering so matches nested under transform groups stay
visible. Implemented as an item-visibility converter over the existing TreeView
rather than switching to SharpTreeView, which would change the Steps contract
and rewrite the pane's tests for no functional gain here.
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
NodeLookup population runs per AST node (and per annotation) on every on-screen
C# decompile, but the debug-step highlighter only consumes it when a step limit
is set. Gate the node-tracking token writer on StepLimit so the common Release
path skips the bookkeeping; AvaloniaEditTextOutput is an ISmartTextOutput, so it
still gets full syntax highlighting via the existing branch.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The debug-step highlight centred with a logical (line-number * line-height)
calculation posted at default dispatcher priority. That ignores collapsed
foldings above the target, races the layout of a just-applied document
(silently no-oping when the viewport is not yet measured), and does not recheck
the document, so a newer decompile could scroll the wrong content. The symptom
was unreliable centring, most visible in the ILAst view whose nested block
output is fold-dense and larger.
Route the highlight through the existing CenterLineInView helper that bookmark
navigation already uses: it centres via GetVisualTopByDocumentLine (fold-aware),
runs at Background priority so the document finishes measuring first, and
rechecks the document identity.
Assisted-by: Claude:claude-opus-4-8:Claude Code
MarkNodeStart and BeginSpan captured builder.Length to anchor a node range
or highlight span, but indentation is written lazily on the first token of a
line. A node or span opened at the start of an indented line therefore
recorded its start before the leading tabs, so the debug-step highlight (and
any span) extended back across the indentation to column 0.
Flush the pending indent in both before capturing the offset, matching the
WPF AvalonEditTextOutput.BeginSpan the Avalonia port derived from. The emitted
text is unchanged -- the indent is written either way, in the same place; only
the recorded start moves to the first real character.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Debug Steps "show state before/after" replay re-decompiles the active
tab; the headless UI tests inferred completion by polling IsDecompiling +
Text, which can return on stale state or hit the 60s wait deadline when
that shared signal races under CI load (the observed intermittent CI
timeout). RestartDecompileWithStepLimit now returns the decompile Task so
the replay completion can be awaited deterministically; the tests await it
instead of polling. The production IsDecompiling reset is unchanged -- the
last decompile's finally always resets it; the race was only in the test's
completion inference.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The C# debug-steps view highlights and centers the exact AST node a
transform changed; the ILAst view already had the step tree and
replay-at-step but produced no highlight. Bring it to parity.
IL rendering has no token-writer seam like the C# output visitor, so
per-instruction text spans are recorded by bracketing
ILInstruction.WriteTo via a new INodeTrackingOutput. The dominant
inst.ReplaceWith(newInst) transform pattern detaches the instruction
passed to Step, so ILTransformContext gains EndStep to record the
produced instruction; Stepper additionally records the position's
ancestor chain as fallback candidates before the step-limit throw, so
the "show state before" view -- which halts at the selected step --
still resolves to a surviving ancestor (ultimately the ILFunction).
The highlight-range resolver is shared with the C# language.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Place the selected debug-step node in the middle of the editor after replay so navigation lands with enough surrounding context instead of just barely scrolling the mark into view.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
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
ILSpy resolves an assembly's references against the target framework it
detects from the TargetFrameworkAttribute. When that attribute is missing,
wrong, or the user wants to force a different framework, there was no way to
hint the correct one, so references could resolve against the wrong runtime
pack or framework directory.
A LoadedAssembly can now carry a TargetFrameworkIdOverride that short-circuits
detection (it is the single value every LoadedAssembly-based resolution path
reads), is persisted in the assembly-list XML, and is carried across a reload
so a runtime change re-resolves against the new framework. The "Set Target
Framework" context-menu entry edits it through a dialog with a free-form text
box and an always-visible list of common monikers to pick from (the app forces
overlay popups, so a dropdown would be clamped inside the small dialog); input
is validated and converted from the short TFM users know (net48) to the long
FrameworkName form the resolver consumes (.NETFramework,Version=v4.8) via
NuGet. The direct DetectTargetFrameworkId callers in the decompiler core
(project export, language version) intentionally keep reading the real
attribute; only reference resolution is overridden.
Resurrects a 2020 prototype (branch tfmoverride) re-implemented against the
current ILSpyX/Avalonia code, whose surrounding structures no longer matched.
Assisted-by: Claude:claude-opus-4-8:Claude Code
- Extract MatchesBookmark for the token + assembly identity that tied a bookmark
to a member; GetLineForBookmark and DocumentContainsBookmarkToken each spelled
it out (four copies), so a change to the match rule had to be made everywhere.
- Add a FoldingSection overload to FoldingsViewState.Restore and route the
bookmark folding restore through it, dropping a duplicated checksum + match loop.
- When two bookmarks resolve to the same gutter line, draw the enabled glyph so
the line never reads as disabled while an active bookmark sits on it.
- Drop an out-of-band reference in a test comment.
Assisted-by: Claude:claude-opus-4-8:Claude Code
BookmarkManager carried a byte-for-byte copy of ILSpySettings' private
MutexProtector, both guarding read-modify-write of a config sidecar in the same
settings directory so parallel ILSpy instances fold their edits in turn instead
of clobbering each other. Promote it to a single public type in
ICSharpCode.ILSpyX.Settings so the locking semantics live in one place.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two costs on the gutter's hot path:
The margin rebuilt its document-line -> glyph map on every Render, resolving
each bookmark by scanning the document's reference segments. A scroll, a hover,
or the 600 ms post-navigation pulse repaints many times a second, so this
rescanned the references per bookmark per frame. Compute the map once and reuse
it, clearing the cache only when the document or the bookmark set changes (the
model's DebugInfo/References are set before the text, so the document-change
hook sees them current).
CanToggleBookmarkAtLine, called for every gutter line the pointer moves over,
built a full bookmark including a captured editor view state -- a foldings
snapshot, scroll offsets, and a tree-path walk -- only to test it for null. It
now does an anchor-only check; the view state is captured only when a bookmark
is actually created.
Assisted-by: Claude:claude-opus-4-8:Claude Code
- Route the bookmarks pane's double-click and next/previous navigation through
HandleExceptions() rather than discarding the Task with `_ =`, so an exception
raised during navigation reaches the global handler instead of vanishing.
- Drop the unused parameterless NextDefaultName() overload.
- Rename the misspelled Boomark.Disable.svg asset (and its loader string) to
Bookmark.Disable.svg.
Assisted-by: Claude:claude-opus-4-8:Claude Code
After scrolling to a navigated bookmark, the deferred apply restored the
caret/scroll captured when the bookmark was created, which overwrote the
centering that had just been computed for the resolved line. A bookmark
re-anchors by token / IL offset, so a decompiler-setting change that reflows
the C# moves it to a different line than the one saved in its view state; the
stale offset then scrolled that line back off-screen, leaving only the
highlight playing where it could not be seen.
Restore just the captured foldings now -- and before centering, since
collapsing or expanding shifts where lines sit -- and let the centered,
re-resolved line be the final caret and scroll position.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Every save reloaded the JSON sidecar into fresh instances and then replaced the
whole observable collection with Clear()+Add(). That dropped and re-added every
bookmark on each toggle or edit: the bookmarks pane keeps its selection by
reference, so it was cleared on every change (leaving the toolbar acting on a
stale or null selection), the grid's scroll reset, and a checkbox/name edit made
from the grid re-entered the DataGrid mid-edit while its ItemsSource was torn
down and rebuilt.
Reconcile in place instead: keep the existing instance for any anchor still
present (refreshing only its editable Name/Enabled), remove anchors that are
gone, and append new ones. A name/enabled edit leaves the membership unchanged,
so it now touches the collection not at all; structural changes touch only the
affected rows. Instance identity is preserved, so the pane's selection and the
gutter's references survive.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two fixes to the gutter's hover preview, the faint glyph shown on a hovered
bookmarkable line:
A removal click left the pointer hovering the line it had just cleared, so the
hover preview redrew a glyph there immediately and the click looked like a
no-op. The just-removed line's preview is now suppressed until the pointer
leaves that line; a jitter that stays on the same line keeps it hidden, and a
fresh hover after leaving shows it again.
The preview also never actually faded: the glyph is an SvgImage, which paints
through a custom Skia draw operation that ignores DrawingContext.PushOpacity,
so it always rendered fully opaque. The glyph is now rasterized once into a
bitmap, which DrawImage does composite at the pushed opacity.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The text area paints the I-beam across its whole surface, so the bookmark
icon gutter, line numbers, and folding margin inherited it by walking up the
visual tree. Those margins are click targets, not text, so the I-beam read
wrong over them. Force the normal arrow there, re-applying as the line-number
and folding margins come and go with their display settings.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Import read the chosen file through the tolerant sidecar-load path, which maps a
missing, empty, or unparseable file all to an empty list. Replace mode then
cleared the saved list and wrote the empty result back, so picking a corrupt or
non-bookmark JSON file for a Replace import silently destroyed the user's
bookmarks.
Import now reads through TryReadFrom, which distinguishes a valid (possibly
empty) file from a parse/read failure. A failure leaves the live and saved lists
untouched and returns false so the pane can report it; a valid empty file still
legitimately clears the list in Replace mode. The normal sidecar load keeps its
tolerant behavior.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Activating a bookmark only positioned the view when a decompiler tab was
already the active content: the pending bookmark was set on
DockWorkspace.ActiveDecompilerTab, which is null while a metadata table, the
Options page, or the About page is showing. Navigating from there selected and
decompiled the node but opened at the top with no line highlight.
The pending bookmark now travels to the decompiler model that ShowSelectedNode
routes the selection to, and is consumed in the document-apply step alongside
the view-state restore -- mirroring PendingViewState -- rather than reacting to
a property change. The earlier property-change path fired synchronously during
selection, scrolled, then a deferred document apply reset the caret to the top
with nothing left to re-apply. For a node that is already decompiled (re-shown
after an interlude, where no apply step runs) a ScrollToBookmark callback on the
model scrolls the live view directly, mirroring NavigateBookmarkInFile.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Bookmarks now capture and restore the requested decompiler view state, support rendered-line fallback anchors, expose hover and pane location affordances, route bookmark context-menu actions through the clicked offset, and update the JSON sidecar under a mutex so concurrent instances do not overwrite unrelated bookmark edits.
The plan file records the completed checklist and the remaining manual smoke-test gap.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
Tidies up the bookmarks feature in response to review feedback:
- BookmarkMargin subscribed to the shared BookmarkManager.Changed event
in its constructor and never unsubscribed, so the singleton kept a
closed tab's margin (and its pulse timer) alive. The subscription now
follows the margin's time in the visual tree.
- LineHighlightAdorner.DisplayLineHighlight always added a fresh renderer
with its own timers; navigating repeatedly within the highlight
lifetime stacked them. Existing highlights are now dismissed first.
- ApplyOutput always built a new DecompiledDebugInfo, contradicting the
property's documented contract (null for non-C#, the Empty sentinel
when C# yielded no methods). It now honors that contract.
- The throwaway StringWriter used for sequence-point capture is disposed.
The MemberName doc comment still described a stale-token guard that was
deliberately removed: a token can resolve to a compiler-generated member
(e.g. a local function) whose name differs from the stored display name,
and a name check would wrongly reject a valid navigation. Comment fixed
to match the actual token-only navigation.
Assisted-by: Claude:claude-opus-4-8:Claude Code
There was no way to mark and return to interesting spots in decompiled
code. This adds a flat (no folders, no labels) bookmark list: toggle on a
line from the context menu, the gutter, or Ctrl+B; see an icon in a new
left-margin gutter that honours a disabled state; and manage the list in a
dockable pane that auto-registers in the Window menu.
Bookmarks anchor by metadata token, never by a raw line number, so they
survive re-decompilation and decompiler-setting changes that reflow the C#
text: a definition line anchors to its token, while a line inside a method
body anchors to the method token plus an IL offset. Recovering an IL offset
needs the decompiler's sequence points, which the normal C# output did not
carry, so they are captured once at the WriteCode chokepoint and stored as a
per-document line/offset map. The map is also what places gutter icons and
scrolls navigation to the exact line.
The list persists to an ILSpy.Bookmarks.json sidecar next to ILSpy.xml; the
path logic is extracted into AppEnv/ConfigurationFiles so the dock layout
sidecar shares it. Navigating to a bookmark loads its assembly from disk if
it dropped out of the list (and only then offers to remove a bookmark whose
file is gone), then centres the line and plays a brief line flash plus a
gutter-icon pulse. Disabled bookmarks stay visible but are skipped by the
next/previous actions.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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
The IL view renders a macro opcode like ldarg.0 as the mnemonic (an opcode
reference) followed by the index written as a separate clickable local or
parameter reference. AvaloniaEditTextOutput's omitSuffix path used
TrimEnd('.') to drop the index, but the name ends in the index digit, not a
dot, so it was a no-op: the full "ldarg.0" was written and the index appended
again, producing "ldarg.00" (and likewise ldloc.NN / stloc.NN).
PlainTextOutput and the WPF AvalonEditTextOutput strip everything after the
last dot; the Avalonia port regressed this. Restore that behavior. Only the UI
was affected -- ilspycmd and the disassembler tests use PlainTextOutput.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ReadWin32Resources walked the PE resource directory tree with raw native pointer
arithmetic over attacker-controlled offsets, counts and sizes, with no bounds
checks, no recursion-depth limit and no cycle detection. The root section pointer
came from GetSectionData, whose length was read and then discarded, leaving every
dereference unbounded.
A crafted assembly could therefore turn merely opening it (the Save as project
feature reads these resources unconditionally) into an uncatchable process kill or
an out-of-bounds native read: a subdirectory entry pointing back at itself recursed
until the stack overflowed; an inflated entry count walked off the section end; and
a data entry whose Size was up to 4 GB made Buffer.MemoryCopy read far past the
section, faulting on an unmapped page or copying adjacent process memory into the
byte[] later written to app.ico/app.manifest on disk. None of this is containable,
since a StackOverflowException cannot be caught and the repo has no corrupted-state
exception handling. This is the sibling of the bundle signature fix in a154a7bbb.
Carry the section length alongside the root pointer and bounds-check every offset,
entry count, name-string length and data Size against it, cap recursion depth and
track visited directory offsets to break cycles. A hostile or truncated file now
yields a bounded, partial tree instead of a crash; well-formed resources parse
exactly as before. The parser no longer needs the whole PEReader, only a delegate
that resolves a data RVA to a bounded pointer, which is the seam the new tests drive
over a pinned buffer.
Assisted-by: Claude:claude-opus-4-8:Claude Code
SingleFileBundle.IsBundle scans for the 32-byte bundle signature and reads the
8-byte header offset stored immediately before it. That offset only exists in a
genuine bundle, where the signature sits near the end of the file. The scan
started at the first byte, so a crafted file with the signature at offset 0..7
made it read before the start of the buffer. On the production path that buffer
is a page-aligned memory-mapped view, so the read faults on the preceding
unmapped page with an AccessViolationException -- a corrupted-state exception
that bypasses the loader's catch and terminates the process merely from opening
the file. The bundle probe runs on every opened file, so this needs no user
action beyond open.
Skip the backward read unless the match is at least sizeof(long) bytes into the
buffer. While in the same file, bound ReadManifest's FileCount against the bytes
that remain before it pre-sizes the entry array, so a crafted manifest can no
longer request a multi-gigabyte allocation.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ILFunction.IsAsync is derived from the method signature, so .NET 11
runtime-async methods (MethodImplAsync bit, no MoveNext state machine)
report IsAsync without AsyncAwaitDecompiler ever populating
AsyncDebugInfo. Its Awaits then stays an uninitialized ImmutableArray,
and PortablePdbWriter threw an NRE building the MethodSteppingInformation
blob from that default struct. Runtime-async methods have no yield/resume
offsets to record, so guard on Awaits.IsDefault and omit the blob,
matching the C# compiler, which emits no stepping information for them
either. A genuinely zero-await classic state machine keeps an
initialized empty Awaits and is unaffected.
Assisted-by: Claude:claude-opus-4-8:Claude Code
DebugInfoGenerator asserted that every local variable's decompiled type
is equivalent to the metadata local-signature type at its slot. That
holds at IL-read time but not afterwards: variable splitting gives one
slot several typed variables, pinned-region locals are modeled as
pointers (int* vs int& pinned), and generic-context type-parameter
identity differs. The assertion therefore aborted PDB generation (Debug
builds) for ordinary inputs such as any method with a fixed statement.
The type is never written to the PDB - only the slot index and name are -
so the mismatch cannot affect the debugging experience. The slot index,
the only emitted value, is correct by construction: it is the IL
ldloc/stloc operand, sourced from the signature slot when the variable is
created and copied verbatim by SplitVariables; it is never reassigned.
Keep only the index-bound check and document why the type is not verified.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Decompiler warnings (ILFunction.Warnings, e.g. the DetectPinnedRegions
block-duplication notice) are surfaced as an EmptyStatement carrying only
a comment. VisitEmptyStatement prints no semicolon for it, and
EmptyStatement derives its StartLocation/EndLocation from its Location
field, which is only set when that semicolon token is written. The
statement was therefore left without a text location, and
SequencePointBuilder then asserted on the empty start location while
generating PDB sequence points - aborting PDB generation for any assembly
whose decompilation emits such a warning (e.g. System.Net.Requests).
Point the empty statement at the comment it carries (already printed by
the time the node ends, so its location is known), falling back to the
collapsed end-of-last-token position. Every printed node then has a
location, so the SequencePointBuilder invariant holds without
special-casing, and the statement lines up with the text it represents.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A standalone portable PDB (or raw metadata stream) loads as a metadata-only
MetadataFile, but the tree node was left empty: LoadChildren early-returned for
any kind other than PortableExecutable/WebCIL, so the node showed an expander
over no children. This regressed in the WPF -> Avalonia port, which dropped the
old default switch arm; release/10.1 still populated such nodes with the
metadata table and heap nodes directly. Restore that behavior.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The pretty DynamicTests exercised arithmetic and relational binary operators on
dynamic operands, but never the bitwise and shift operators (& | ^ << >>). Add a
BitwiseAndShiftBinaryOperators case so this operator family is pinned by a
round-trip test alongside the others.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The pretty DynamicTests only exercised += -= *= /= on a dynamic target, leaving
%= &= |= ^= <<= >>= unverified even though VisitDynamicCompoundAssign and
GetAssignmentOperatorTypeFromExpressionType already map them. Add them so the
full set of dynamic compound-assignment operators is pinned by a round-trip
test.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The correctness DynamicTests only ran binary + - * / on a dynamic operand, so
no test recompiled and executed decompiled output for any unary operator. That
gap is why ~ on a dynamic value (issue #3820) shipped uncompilable output
undetected: a correctness case round-trips the decompilation through the
compiler, so it fails the moment the decompiler emits something that does not
recompile.
Add ~, -, +, and ! cases. They pin the runtime semantics of the dynamic unary
path and would have caught the OnesComplement regression directly.
Assisted-by: Claude:claude-opus-4-8:Claude Code
VisitDynamicUnaryOperatorInstruction handled every dynamic unary operator
except ExpressionType.OnesComplement, so ~x on a dynamic operand fell through
to the unsupported-opcode error expression and produced uncompilable output
(an incomplete cast that fails to parse). Map it to the bitwise-complement
operator, like the sibling unary cases.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
NullPropagationTransform rewrote `c != null ? c.AccessChain : default` to
`c?.AccessChain ?? default` whenever the access-chain result was a non-nullable
value type. For a by-ref-like type (a ref struct such as Span<T>) that form does
not compile: a ref struct cannot be wrapped in Nullable<T> (CS8978). Exclude
by-ref-like return types from the null-coalescing rewrite.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
The PDB sequence-point tests were missing real while-loop input, and their residual comparison treated breakpoint locations as an unordered multiset. Add coverage for while/do-while fixtures and compare residuals in sequence order so stepping-order changes are pinned.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
Extends the breakpoint-map comparison to hidden sequence points, anchoring
each hidden point to the visible point it follows so the descriptor stays
independent of the IL offsets the decompiler reconstructs. Adds PdbGen cases
spanning try/catch/finally, switch, async/await, yield, loops, LINQ, pattern
matching and more, pinning the known residuals where the decompiler folds a
compiler-hidden branch into an adjacent point.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The PdbGen tests compared the reconstructed PDB to the C# compiler's
byte-for-byte, so any non-trivial method failed on reconstructed IL
ranges, hidden sequence points and local scopes - none of which the
decompiler can reproduce exactly. That left four of seven fixtures
[Ignore]d and the suite with almost no coverage.
Compare only what a debugging user actually feels: the visible (non-hidden)
breakpoint map, parsed straight from the sequence-point blobs and keyed by
method-definition row (shared between the PDB and the PE it describes). IL
offsets, hidden points, local scopes and the embedded source are dropped.
The compiler's own PDB is the oracle, so the tests assert correct debugging
behavior rather than the decompiler's past output. Methods where the
decompiler legitimately diverges pin an auto-derived residual snapshot, the
same accept-the-diff workflow as the pretty tests; a separate oracle-free
check rejects duplicate or overlapping sequence points.
Un-ignores ForLoopTests, LambdaCapturing and Members (its source is
regenerated to match the decompiler's per-type output, collapsing ~50 lines
of indentation-induced coordinate noise to two genuine differences).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The "Toggle folding" context-menu entry folded the block at the caret, so
right-clicking a fold on a different line than the caret toggled the wrong
block. Record the document offset under the pointer when the text-view menu
opens and expose it on TextViewContext as TextLocation; the folding entry acts
on that offset (falling back to the caret only when no click position was
recorded). The pointer handler now records the offset before the reference
lookup, so a document with foldings but no clickable references still gets a
position.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The analyzer tree set ShowLines="False" while the assembly tree uses "True", so
the Analyze panel was missing the connector lines the rest of the app shows. The
two panes already share the same SharpTreeView control and templates, and the
line geometry is driven entirely by the generic node Level/IsLast state, so the
analyzer hierarchy renders them correctly; the False was an unconsidered default
carried in when the pane was migrated to SharpTreeView. Align it with "True".
Assisted-by: Claude:claude-opus-4-8:Claude Code