The await surface had almost no fixture coverage beyond Task/ValueTask: every
GetAwaiter in the corpus was an instance method on the awaited type itself, so
the conversion VisitAwait applies to the operand was never exercised for an
inherited, interface-typed or extension-method awaiter. Probing that surface
turned up eight defects, all of which produce C# that does not compile.
AsyncAwaitPatterns pins the shapes that do round-trip, along the three axes the
translation actually depends on: the GetAwaiter receiver, the operand
expression, and the context the await sits in. Its Correctness twin pins what
Pretty cannot see - copy semantics of struct awaitables and the evaluation
order around the suspension point.
AsyncAwaitPatternsBugs is the spec for the defects, written as the C# that
ought to come out, with the current wrong output named per member. It fails
today; that is the point, and fixing a defect is meant to delete a comment
rather than edit an expectation.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The registry's summary still described the static accessor as resolving
it once, which stopped being true when the accessors began going through
the current composition host on every access.
Assisted-by: Claude:claude-fable-5:Claude Code
The app-level NativeMenu is process-wide, so the withdrawal a window does
on Closed has to name the items that window put there. Withdrawing
"whatever is promoted right now" is correct only while one window exists
at a time: with two, closing the older one takes the newer one's About /
Check for Updates out of the macOS app menu, and nothing ever puts them
back. Not reachable today - MainWindow is [Shared] and Attach runs from
its ctor - but the failure mode is silent and permanent, and carrying the
list costs nothing. Removing an item that is already gone is a no-op, so
a superseded window's Closed stays harmless.
The promotion tests also have to leave the app menu as they found it:
it is declared on Application and outlives the test, it is not gated on
macOS, and on Windows and Linux nothing re-promotes over the leftovers.
Assisted-by: Claude:claude-opus-5:Claude Code
ProgressBar.IsIndeterminate defaults to false, so the "nothing is running
yet" assertion would also pass against a pane whose DataContext is not
the resolved SearchPaneModel, and the failure would only surface one
line later, blamed on the binding direction rather than on the missing
DataContext.
Assisted-by: Claude:claude-fable-5:Claude Code
The app-level NativeMenu declared in App.axaml lives as long as the
process, while every MainWindow builds its own Help items over its own
command instances (AboutCommand reaches the DockWorkspace and, through
it, the whole app graph). PromoteHelpToMacAppMenu inserted each window's
items without taking the previous window's out and nothing removed them
on close, so on macOS the headless suite kept every test's app graph
alive - the same 13 MB per test as the anchors fixed earlier on this
branch, and the reason the memory win did not reproduce on macOS
(retained gen2 still climbing to ~4 GB there while a Windows run peaks
at 0.7 GB). Forcing the macOS path on Windows reproduces the growth
(14.3 GB peak private bytes over the suite); withdrawn, it is 0.7 GB.
Three smaller anchors of the same kind, found while making the canary
below hold in the full suite: RichNodeText and AnalyzerTreeNode cached
the first container's exports in statics, which subscribed later
windows to a stale settings object, handed later analyzers the first
test's assembly list, and kept the first app graph reachable for the
run; and a search still in flight when its container went away kept its
drain timer and IsSearching - hence the pane's indeterminate progress
animation on the render clock - alive, retaining every window a search
test closed mid-run (about 30 of them, ~400 MB).
The canary test closes a MainWindow the way the per-test teardown does
and waits for it to become collectable. It fails on any single anchor
being restored (checked by leaving DetachFlyouts out), which is the
regression guard the individual anchor fixes lacked; the teardown body
is exposed as TearDownTestState so the test performs exactly what
AfterTest does.
Assisted-by: Claude:claude-fable-5:Claude Code
The search pane's progress bar was permanently indeterminate and merely
hidden when idle, and the decompiler view's bar defaults to
indeterminate mode whether or not a decompilation is running. The
indeterminate indicator is an infinite keyframe animation that keeps
running - and keeps the control's whole visual tree alive through the
render clock - for as long as the pseudo-class is set, hidden or not.
Both bars now go indeterminate only for the duration of the work.
Assisted-by: Claude:claude-fable-5:Claude Code
WritingOptions is process-wide static state, and the pane subscribes to
its PropertyChanged in the constructor. Every composition container
that is built and disposed (the headless UI test suite does that per
test) left its pane behind on that event, and through the pane's
LanguageService the rest of the container's object graph with it.
System.Composition disposes IDisposable shared parts with the
container, which is the moment to let go.
Assisted-by: Claude:claude-fable-5:Claude Code
The headless test host runs the app without an application lifetime,
so the window-closing step in ResetAppState never had a list to work
from and every MainWindow the suite showed stayed open - and reachable
from the compositor, together with its view-models, assembly tree and
loaded assemblies. Measured at about 13 MB per test, 15 GB over the
suite, enough to page out the CI runner and stall the tests that scan
process module lists.
Closing is not sufficient on its own: Avalonia's Button subscribes to
its flyout's Opened/Closed and only unsubscribes when the Flyout
property changes, and Dock's ToolChromeControl theme hands every tool
pane's chrome button one shared MenuFlyout resource, which therefore
pinned every closed window's visual tree. The flyouts are detached
before the window closes.
Assisted-by: Claude:claude-fable-5:Claude Code
The preferred-scale lookup reaches well past the byte-normalization cases it was
added for: any value that is exactly n/2^k for a k the old denominator limit
could not reach now prints as a fraction, so constants that used to be short
exact decimals changed shape. That is the widest-reaching part of the change and
nothing pinned it.
The added constants are those values, including the unreduced 126 / 1024 that a
lowest-terms rewrite would turn into 63 / 512, plus two that must keep their
decimal form so the length gate stays covered from both sides.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Resolving a metadata file to its assembly node learned to descend into packages,
but three sibling lookups kept their own scan of the root's direct children, so
a token reference, a metadata:// link and a LoadedAssembly reference still
resolved to nothing inside a package. One of them sat behind a guard whose
result was never used, which returned early for exactly the case it was meant to
serve. Routing all of them through the one lookup fixes them together.
Namespaces were matched by comparing a full name against a node label, which is
only ever equal in flat mode: with nested namespace nodes the label is the last
segment, and the empty-name test matched the first child rather than the global
namespace node. The assembly node already indexes its namespaces by full name.
The descent itself no longer sweeps the package depth-first. Expanding a folder
resolves and extracts every .dll it holds, so the path is taken from the
package's folder graph, which costs no tree node and reads no entry.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Searching a package resolves its entries by file name against a case-insensitive
cache, which is right for an assembly reference but wrong for an archive entry:
two entries differing only in case are two files, and they collapsed onto one
LoadedAssembly, so one was searched twice and the other never. Keying the cache
by the entry itself separates them, and the entry's package-relative path
becomes the assembly's file name, which is what tells the copies of one assembly
in a multi-target package apart wherever a search result shows a location.
Cancellation was only checked between top-level list entries, so a walk the user
had already replaced by typing another character kept extracting package entries
alongside the run they were waiting for. The omnibar had no way to end its run at
all: its view model is per document tab and nothing cancelled it when the tab
went away.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
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 sign of a constant cannot decide whether to emit a unary minus: MinValue
and NegativeInfinity are negative, yet are their own members and must not be
negated. Deriving it that way emits -float.MinValue for float.MinValue, which
is a different value.
Which constants are reachable by negation is also not obvious: -MaxValue is
exactly MinValue and both infinities have their own members, so Epsilon is the
only one, but establishing that takes a proof rather than a read. Recording it
in the table states the invariant instead, and leaves the lookup itself as the
single dictionary probe it was before.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Resolving a type to its tree node scanned every descendant of the root, which
means every namespace node of every assembly - all of them built eagerly - to
find the one assembly node it needed. A package child records the bundle it
came from, so that chain leads straight to the single top-level node worth
descending into, and only that package's folders are searched from there.
The two sibling lookups only ever considered the root's direct children, so
neither resolved anything inside a package at all. Sharing one helper fixes
them along the way, and it expands package folders on the descent because
search surfaces package contents whether or not the tree was ever opened
there.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
dotrush resolves the active target and owns the build task, so the launch and
attach configurations go through its commands. Those commands only exist when
the extension is installed, so a third configuration builds and launches the
apphost directly for setups without it.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Searching inside bundles and packages means expanding them, and the expansion
has to await each assembly's load result - which is what triggers the lazy
load in the first place. Building the full list up front (as the WPF pane did)
therefore means a search on a freshly restored list produces nothing at all
until the last assembly is off disk, and the blocking wait for it ignored the
cancellation token the pane fires on every keystroke.
The snapshot is still taken eagerly, before the first element is yielded, so
the set cannot change under a running walk; a failing assembly or an
unreadable package entry skips itself rather than abandoning the rest.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A stalled request to nuget.org travelled out of the package run as an unhandled
exception, so the report filed it as a decompiler [EXCEPTION] - the one bucket
that has to hold nothing but real crashes - and the package was skipped without
a single type being decompiled. Seen in the 2026-08-16 sweep, where
common.logging.log4net timed out resolving its version list and decompiles
clean on a second attempt.
A 404 stays immediate: it is an answer, not a flake, and the sweep asks about
plenty of ids that are not packages.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Both tools have found real decompiler defects (several merged fixes came out of
nugetfuzz sweeps), but they only existed in a private checkout, so nobody else
could run them and their setup knowledge lived in one head. They complement the
fixture suite from the other side: it decompiles code we wrote, these decompile
what the world ships.
They stay outside the solution - file-based apps, run by hand, never by CI - and
the near-empty Directory.Build.props/Directory.Packages.props keep the repo-wide
warnings-as-errors, lock-file and central-package-management settings from
reaching them.
The catalog sweep driver is PowerShell rather than bash so it runs on Windows as
well, which also drops its curl/jq dependency; staging falls back to copying when
Windows withholds symlink privileges, and report file names are hash-truncated to
stay inside the 260-character path limit.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The dump is only useful if it shows the same thing the real pipeline is fed and
covers everything a transform bug can hide in, so: accessor bodies are reached
through the metadata handles (ITypeDefinition.Methods hides every method that
has method semantics), the PDB reaches the ILReader (UseDebugSymbols alone is
inert without DebugInfo), and the writing options carry the same sugar as the
UI's ILAst pane.
A method whose body cannot be read, transformed or written no longer aborts the
run, and the failure now travels through the decompilation-error path, so a
crashing transform is visible in stderr and in the exit code instead of being
buried in the output a script just collected.
BlockILTransform entries name the transforms they contain: two of them ran as
identical rows before, and asking for a nested transform by name reported it as
unknown while the listing did in fact run it.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The decompiler's intermediate representation was reachable only through the
GUI's ILAst language, so anyone debugging a transform or a matcher had to do it
by hand in the UI. --ilast writes the same representation to stdout, and
--after-transform truncates the pipeline at a chosen point, which makes the
effect of a single transform diffable and scriptable.
Debug-only, like the UI language it mirrors: ILAst serves ILSpy's own
development, not the users of the released tool, so it stays out of the shipped
NuGet package and out of the README's option list.
Nested per-block transforms stay unaddressable: they run inside a
BlockILTransform entry, and reaching them needs the Stepper, which is compiled
out of release builds anyway.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The rich hover popup hardcoded a near-white background and border while
its signature text is coloured by the active highlighting theme, so in
dark mode light-on-dark syntax colours landed on a light box and were
unreadable. The chrome and the doc-link colour now route through
theme-variant brushes; light mode keeps the established near-white look.
Assisted-by: Claude:claude-fable-5:Claude Code
NativeMenuItem.Gesture is display-only when NativeMenuBar renders the
menu inline on Windows/Linux: the managed fallback binds it to
MenuItem.InputGesture, which never handles input. Only macOS's system
menu bar actually executes its key equivalents, so Ctrl+O, Ctrl+S and
F5 showed in the menu but did nothing. The Avalonia docs call this out
explicitly: InputGesture only displays the text and must be paired with
a KeyBinding for the shortcut to function.
Assisted-by: Claude:claude-fable-5:Claude Code
The dark palette is hand-authored for C# only; every other highlighting
definition -- XML, IL, Asm, and all AvaloniaEdit built-ins -- is derived by
inverting HSL lightness. HSL lightness is not perceptual luminance, so the
result depended entirely on hue: blue carries a 0.0722 luminance weight, so
plain Blue landed at 4.08:1 against the editor canvas, and an already-light
source such as Asm's #8080FF inverted downwards to 1.29:1 -- invisible.
Reported against XML resources in #3986.
Enforcing a 5.5:1 WCAG floor on the converted foreground fixes every affected
definition in the one place they all route through, which a per-language
palette would not: the AvaloniaEdit built-ins (JSON, Markdown, JS, HTML, CSS,
Python) have no palette to author. 5.5 is where the existing CSharpDark values
already sit; the 4.5 AA threshold was measured and only moves the reported blue
to 4.51. The floor is deliberately foreground-only -- forcing a span background
to contrast with the canvas would repaint Asm's #EEEEEE Registers background as
a bright block and bury the text on top of it -- and it is measured against the
surface the foreground lands on, which is that span background when the colour
declares one, so a light-on-dark span cannot be pulled apart into two colours
that no longer contrast with each other.
The same function's desaturation guard only fired when the inverted lightness
stayed below 0.75, so a dark fully saturated source (DarkMagenta) came back
light and still fully saturated -- exactly the neon the softening exists to
prevent. Only the softening becomes unconditional; the lightness lift paired
with it stays scoped to over-saturated colours, because it is not monotone
across its own 0.75 boundary and would reorder neighbouring greys.
Hyperlinks were a second, unrelated path: nothing ever set
TextView.LinkTextForegroundBrush, so the About page and every decompiler-view
link used AvaloniaEdit's registered default of pure blue, 1.94:1 on dark. They
now share a themed ILSpy.LinkForeground with the metadata table's token cells,
which take it from a style rather than a local Foreground so the selected row's
white override still wins over the accent fill.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
An anonymous parameter type cannot be named, so the original lambda must
have used implicit parameters throughout. Emit the whole parameter list
implicitly instead of mixing implicit and explicit declarations.
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
A negative constant operand is usually the two's-complement rendering
of a bit mask or a high unsigned value (an enum member, a sentinel);
the IL view now appends the hexadecimal form as a comment, e.g.
'ldc.i4 -501 // 0xfffffe0b' (#1142). The short forms stay bare: their
operand range is readable as-is. The disassembler round-trip comparer
strips comments, so the new NegativeConstants case pins the rendering
with explicit content asserts.
Assisted-by: Claude:claude-fable-5:Claude Code
An addition or subtraction on an enum whose constant operand's numeric
value does not fit the underlying type (an int constant standing for a
high uint member, e.g. -501 for 0xfffffe0b) failed to resolve as enum
arithmetic and fell back to integer arithmetic with casts, producing
'(uint)((int)value - -501)' and, for the same source expression in an
argument position, '(uint)value - 4294966795u'. Retry the failed
resolution once with constant operands reinterpreted in the enum type;
the reinterpretation is lossless whenever the constant's stack type
matches the enum's underlying stack type, because the IL constant is
the member's bit pattern. Valid non-enum resolutions like 'data - 1'
(enum minus underlying, yielding the enum) are unaffected because the
retry only runs when the plain resolution fails.
Assisted-by: Claude:claude-fable-5:Claude Code
The span and tuple tests need types the legacy reference mscorlib predates, so
each of them opened System.Runtime.dll from the ref-assembly toolset into its own
SimpleCompilation - five copies of the same block, five reads per test run. One
shared lazy compilation covers all of them, and it includes the test assembly so
the operator and extension-method fixtures the conversion tests rely on resolve.
Part of #829.
Assisted-by: Claude:claude-opus-5:Claude Code
The remaining parameter-modifier dimension of the C# 14 rules: an expanded
params call prefers the params ReadOnlySpan overload over params array (the
C# 13 better-params-collection rule) while the normal form keeps the exact
array overload; and a span conversion never binds a ref or out parameter,
so a keyword-less argument picks the by-value overload and the ref/out
keyword must survive decompilation to keep binding the by-ref one. All
expectations come from compiling probes (the negative directions are
CS1503) and everything was green as written - these are lock-downs, not
fixes.
Part of #829.
Assisted-by: Claude:claude-fable-5:Claude Code
Compiling probes with the C# 14 compiler establishes the matrix: a value
argument binds to an 'in ReadOnlySpan<T>' parameter with and without the
span conversion (a temporary is created), while an explicit 'in' argument
demands the parameter's own type (CS1503); and for a by-value/'in' overload
pair, a call without 'in' picks the by-value overload - also through the
span conversion - while 'in' at the call site makes the in-overload the
only candidate (CS1615 with a conversion).
The fixture pins the decompiler side of the same matrix: 'in' must survive
decompilation where it disambiguates the overload pair, and the folded
span-conversion argument must re-resolve to the by-value winner. The
resolver unit tests pin applicability and betterness directly. All of these
were green as written - they fence the implicit-in cast stripping and the
recheck ladder against regressions rather than fixing a defect.
Part of #829.
Assisted-by: Claude:claude-fable-5:Claude Code
Auditing against the first-class-span-types proposal turned up three
deviations, each now pinned by resolver unit tests whose expectations were
established by compiling probe programs with the C# 14 compiler.
Lower-bound type inference recursed into Span<T> targets as another
lower-bound inference, but Span<T> is invariant and the spec demands an
exact element inference there: M<T>(Span<T>, T) with (Span<string>, object)
must fail inference (CS0411), not unify to T=object.
Better-conversion-target compared ReadOnlySpan element types where the spec
compares the span types, admitting numeric and user-defined element
conversions the span types do not share: overloads taking ReadOnlySpan<int>
and ReadOnlySpan<long> are ambiguous (CS0121), not resolvable. The general
mutual-convertibility rule already implements the spec's span-type test, so
the element-level block is simply removed; the ReadOnlySpan-over-Span
identity rule stays, since it deliberately inverts that general rule.
The explicit span conversion did not exist at all, and with it the rule that
user-defined conversions are not considered between span-convertible types.
The visible consequence: string[] to Span<object> classified as an implicit
user-defined conversion via op_Implicit(object[]) plus array covariance,
where the compiler reports CS0266 - only the explicit span conversion
exists. Span conversions are also no longer considered for extension
receivers during method group conversion (CS0123), while invocations keep
them.
Part of #829.
Assisted-by: Claude:claude-fable-5:Claude Code
The C# 14 compiler lowers implicit span conversions to calls -
MemoryExtensions.AsSpan(string), ReadOnlySpan<T>.CastUp, and the span
op_Implicit operators - so decompiled code showed the lowered form even
though the conversion and betterness layers already implement the C# 14
rules. CallBuilder now folds those helper calls back into conversions,
riding the existing mechanism: the conversion is built as an explicit
cast, consumption sites make it implicit where the context allows, and
the overload-resolution recheck re-adds a cast when the bare argument
would bind to a different overload (which canonicalizes deliberate
AsSpan disambiguations to the equivalent explicit span cast).
Span conversions compose, so CastCanBeMadeImplicit lets a direct
input-to-target span conversion replace a chained pair; and an rvalue
bound to an in parameter gets the same chance to shed the cast as a
by-value argument, since ChangeDirectionExpressionTo bypasses the
by-value strip.
Part of #829.
Assisted-by: Claude:claude-fable-5:Claude Code
The green FirstClassSpanTypes fixture pins overload-resolution behavior
the decompiler already gets right under the C# 14 implicit span
conversions: calls picking the new betterness winners (ReadOnlySpan
over Span/object/IEnumerable, ReadOnlySpan<string> over object[] and
ReadOnlySpan<object>, MemoryExtensions.Contains over
Enumerable.Contains) round-trip as plain calls, while calls picking the
losing overload keep their disambiguating casts and Enumerable.Contains
stays in static call form. Extension methods on span-convertible
receivers, generic inference through span conversions, params
betterness, and array-to-span returns are covered too. All winners were
verified by executing probes compiled at LangVersion 13 vs 14.
The FirstClassSpanConversions fixture is Assert.Ignore'd (#829): it
specs the desired folding of compiler-emitted span-conversion helpers
back into implicit conversions - MemoryExtensions.AsSpan(string),
ReadOnlySpan<T>.CastUp for span variance, and covariant-array/in-arg
conversions - which the decompiler currently renders as explicit helper
calls or casts (recompilable and semantics-preserving, just not
minimal). Both roslyn-latest configs compile the fixture and fail only
at the output comparison.
Assisted-by: Claude:claude-fable-5:Claude Code