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
Below C# 7 ref locals are unavailable, so CopyPropagation is allowed to copy
LdFlda/LdElema. When such a copy lands in a StObj target slot whose value is
impure, it violates the invariant checked by StObj.CheckTargetSlot: C# computes
the value to be stored before dereferencing the target, so the exception moves.
ILInlining resolves the same conflict by marking the address as delayed rather
than falling back to a ref local; copy propagation now does the same, which
keeps the generated code unchanged and only repairs the IL.
Unlike inlining, copy propagation has no third arm to fall back to: by the time
DoPropagate runs, the defining store is about to disappear, so every load has to
be replaced and refusing the copy is no longer an option. That decision can only
be made up front, which is what CanPerformCopyPropagation does when ref locals
are requested. The assertion covers the remaining hole, the public Propagate()
entry point, which bypasses that check -- AsyncAwaitDecompiler copies an ldflda
of the builder field through it irrespective of the setting.
Propagating an address also un-inlines its arguments into fresh stack slots, and
those stores are copy-propagation candidates in their own right. They are
inserted before the store being replaced, so the block scan used to step right
past them: a slot loaded more than once could never be inlined back and survived
into the output as a ref local -- for `s.ShortField >>>= 5` the copied ldflda
left behind `stloc C_0(ldloca s)`, printed as `ref CustomStruct reference = ref
s`. Rewinding the scan to the first of those stores lets them propagate too.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Mandating that follow-up fixes be squashed back into the commit they
amend forces a rewrite of already-pushed branch history, which churns
review threads on open PRs. How to sequence branch commits is left to
whoever owns the branch.
Assisted-by: Claude:claude-opus-5:Claude Code
When fixing a type parameter, Roslyn merges the tuple element names of
bounds that are identical apart from those names: names are kept where
all bounds agree and dropped where they conflict (MergeTupleNames in
Roslyn's MethodTypeInference.cs). The C# standard does not describe
this step. Without it, fixing either kept the first bound's names
verbatim or, with two exact bounds differing only in names, failed
outright - so inferred tuple types could carry names csc would not
produce. All merged-name expectations are csc-verified.
Nullability is deliberately not merged: Roslyn derives it from the
variance of the position, which this implementation does not track, so
bounds that differ in it stay distinct and fixing fails as before
rather than inventing an annotation.
Assisted-by: Claude:claude-fable-5:Claude Code
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Middle_Click_On_An_Assembly_Tree_Row_Opens_A_New_Decompiler_Tab timed out
after its full 60s window on the macOS CI runner. The tests picked the first
SharpTreeViewItem present in the visual tree and clicked its centre, but a
container realised by the virtualizing panel is not necessarily arranged yet,
and a row can sit outside the grid's viewport - on a loaded runner, where
assemblies are still streaming into the tree, the click landed on nothing and
the gesture never happened. The two negative tests shared the same click-point
computation and would have passed vacuously in that state, so a missed click
was only ever visible on the positive one.
Hit-testing the candidate point back to its own row before clicking rules both
out, and fails with a description instead of an unexplained timeout if no row
is ever reachable.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
'this' and 'base' both read the 'this' parameter of the function being
decompiled, but their resolve result did not say so: consumers that key on
ILVariableResolveResult (local-reference output, highlighting, hover) could
not connect the keyword to the variable, and the qualified/unqualified
spellings of the same access carried differently shaped annotations.
The resolver has no ILFunction and thus no variable to put into a
ThisResolveResult, so it stops synthesizing one: LookInCurrentType looks
the name up against the (self-parameterized) current type, which grants
the same protected access, and the annotation of an unqualified field
access is built from the translated target instead. ResolveThisReference
and ResolveBaseReference had no callers left and are removed.
Assisted-by: Claude:claude-fable-5:Claude Code
The hook installs dotnet-format with --add-source, which NuGet rejects
when packageSourceMapping is configured. The dotnet10-transport feed is
already declared in NuGet.config; adding a dotnet-format mapping entry
there lets the install succeed without --add-source.
Assisted-by: Claude:claude-sonnet-4-6:GitHub Copilot
A cast must not reuse an implicit tuple conversion: its elements have to be
classified as cast conversions, which changes the outcome whenever an element
converts through a user-defined operator. Roslyn encodes the same rule in
ClassifyConversionFromTypeForCast via ExplicitConversionMayDifferFromImplicit,
but on our side it rested on an unexplained flag with nothing covering it, so
the flag read as removable. The comments and the test say why it stays.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
TypePair existed only to key that cache, and its hand-written equality
delegated to the same IType comparison the tuple's default comparer performs.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
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
Nothing walked the resolve-result graph: the virtual method and its fourteen
overrides only ever called each other, with InvocationResolveResult's chained
base call as the sole call site in the tree.
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
The resolver's await path has thrown NotImplementedException ever since the
type system rewrite, and nothing else in the repo constructs an
AwaitResolveResult, AliasTypeResolveResult or AliasNamespaceResolveResult:
the decompiler builds await expressions from IL, and alias references never
go through name resolution.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The C# standard does not mention tuple element names in type inference,
but csc merges names across bounds that differ only by them: names are
kept where all bounds agree and dropped where they conflict (Roslyn's
MergeTupleNames). All three expectations are verified against csc.
The two live tests are red at this commit: without merging, fixing
keeps the first bound's element names verbatim. The multiple-exact-
bounds case additionally requires AddExactBound to compare bounds
modulo element names; it stays ignored until that is implemented.
Assisted-by: Claude:claude-fable-5:Claude Code
A call to a value-type constructor is rewritten into
"stobj(target, newobj ...)" because "Struct.ctor(target, ...)" has no C#
equivalent. The rewrite keyed on TypeKind.Struct, so a struct from a
missing assembly resolved as TypeKind.Unknown, fell through to the
ordinary call path and produced a stack-type mismatch.
Metadata cannot settle the question: a TypeRef parent carries no valuetype
bit. The receiver can, though - a constructor invoked with "call" on an
address is a shape only a value type has - so the unresolved case follows
the receiver's stack type and steps aside where metadata does say the type
is a reference type. Reading the receiver has to leave it on the stack for
PrepareArguments, hence the depth-indexed peek.
Assisted-by: Claude:claude-opus-5:Claude Code
Structs whose assembly is missing decompile through the unresolved-type
path, which is not covered anywhere: the fixture pins the constructor
shapes that path has to recognize, and the reference-type cases that must
keep falling through to a plain call.
Assisted-by: Claude:claude-opus-5:Claude Code
Review follow-ups on #3998: reject non-finite parses (NaN slips through
Math.Clamp and, once persisted, permanently fails the editor's
SelectedFontSize > 0 guard), commit the clamped value back into the box on
focus loss (the echo suppression otherwise leaves a typed "3" on screen while
6 pt is stored), and assert the theme actually realizes PART_EditableTextBox
instead of trusting the IsEditable property. The 4/3 pt/px ratio is documented
as the WPF-host convention it is - exact on Windows/X11, deliberately not the
Cocoa-point number on macOS - rather than a universal.
Assisted-by: Claude:claude-fable-5:Claude Code
The options dialog bound DisplaySettings.SelectedFontSize (device-independent
pixels) straight into a NumericUpDown, so a fresh profile showed 13.33 and the
6-72 bounds were pixels. The WPF host presented points via FontSizeConverter;
this restores that behavior on Avalonia with an editable size ComboBox (like
the Windows font dialogs) backed by a pt/px proxy on the viewmodel. The stored
value stays pixels so settings files keep round-tripping with ILSpy 9.x.
Assisted-by: Claude:claude-fable-5:Claude Code
A writable ref-struct argument can receive narrower values through regular ref/out calls, and a ref-return can expose the same storage for field mutation. Treat those paths like receiver captures so inferred declarations remain compilable.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d30b7a7-983d-4efa-8d99-fbface5828dc
Comments used to be child nodes flushed by InsertSpecialsDecorator when the
next node started printing, which put the marker of an init-only setter right
after the keyword. In the slot AST comments are leading/trailing trivia, so the
accessor's trailing trivia moved the marker behind the accessor body. The
placement cannot go back to trivia on the body either: the auto-property
transform drops the body, and the marker with it. The accessor now carries the
init-only fact itself and the printer writes the marker next to the keyword.
Assisted-by: Claude:claude-opus-5: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
ScopedKind is now the authoritative lifetime representation, so retaining
the preview-era boolean fields would duplicate state. Keep the current
ScopedRef compatibility property and group the new metadata attributes
with the other C# 11 attributes.
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