An attribute whose constructor ends in a params array was always printed with
the array spelled out, so omitting the optional arguments in the source turned
into a four-line `new string[] { }` in the output.
Expansion is not unconditional: an attribute may have another constructor that
the shorter argument list binds to instead, which would silently change which
constructor the recompiled attribute picks. Overload resolution over the
expanded argument list decides, and only a result that names the same
constructor in expanded form is written that way.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A C# anonymous type is immutable and compares every member. VB's are neither
unless every property is declared 'Key': otherwise the properties are settable
and only the 'Key' ones take part in Equals and GetHashCode. Writing such a
type as 'new { ... }' silently gave it value equality and made any assignment
to one of its properties fail to compile, so only an anonymous type with no
settable property is treated as one; the rest keep their own declaration.
Those declarations carry the shape VB gave them, so the round-trip preserves
both mutability and 'Key' equality. Their names are the remaining obstacle,
since the VB compiler separates the parts with '$': the type, its backing
fields and any local named after it are renamed to use '_' instead, and a
comment on the declaration says why the type is spelled out.
Generated variable names are now rejected when they would not be legal C#
identifiers, which also stops a display class from lending its unspeakable
name to a local in the NoLocalFunctions output.
Assisted-by: Claude:claude-fable-5:Claude Code
The VB compiler carries the range variables of a query in $VB$It, $VB$It1,
$VB$It2 and $VB$ItAnonymous, its counterpart to C#'s <>h__TransparentIdentifier.
Unrecognized, they were left in place by CombineQueryExpressions, and since '$'
is not legal in a C# identifier every VB query with more than one range
variable decompiled to code that cannot be recompiled.
Assisted-by: Claude:claude-fable-5:Claude Code
FractionApprox rejects inputs above 0x7FFFFFFF because they cannot be stored
as a fraction, but the check was one-sided while the sign is stripped right
after it. A large negative value therefore reached the continued-fraction loop
and overflowed the terms it accumulates. ICSharpCode.Decompiler is built with
CheckForOverflowUnderflow, so that aborted decompilation of the whole member
instead of wrapping.
Found by fuzzing nuget.org; reproduces on MathNet.Numerics, whose constants
reach the approximation through their ratio to PI.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
IsCopyConstructor required the copy constructor to be private on a sealed
record and protected otherwise, but IsGeneratedCopyConstructor in the same
class accepts protected regardless of sealedness. Since the former gates the
latter, a sealed record whose copy constructor stayed protected -- what you get
when a record is sealed after it was compiled -- was not recognized as one at
all: it fell through to the general constructor handling, where its base call
made it count as unchained, and the primary-constructor invariant then failed.
Found by fuzzing nuget.org. Beyond silencing the assertion this improves the
output, as the affected types now decompile to positional records.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The accessor forwarding-stub matcher bounded the IL body from above but not
from below. Reference assemblies keep the method RVA while stripping the body
to zero bytes, so an explicit interface accessor there sailed past the size
check and the first opcode read ran off the end of the blob, aborting the
whole type with a BadImageFormatException. The sibling matcher in
TransformDisplayClassUsage already guards its lower bound; this one did not.
Found by fuzzing nuget.org: every package resolving to the
Microsoft.NETFramework.ReferenceAssemblies packs was affected.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Roslyn compiles top-level statements into a synthesized Program class
whose entry point is called '<Main>$', a name that cannot be declared
in C#. Decompiled output kept it verbatim (escaped to
_003CMain_003E_0024 when exporting a project), and since C# accepts
only a method called 'Main' as an entry point, the exported executable
did not compile (CS5001).
Give that method the name 'Main'. Per the decision recorded in #829 we
do not reconstruct top-level statements, so this is the level of
support the output needs to compile.
An async top-level program needs the name in a different place: it
compiles to '<Main>$' holding the statements plus a '<Main>' entry
point that only awaits it. That wrapper carries the .entrypoint marker
but is hidden from the output, so the name goes to the method it
awaits instead - unless AsyncAwait is off, when the wrapper is
emitted and keeps the name itself.
Assisted-by: Claude:claude-fable-5:Claude Code
decimal has no IL literal: legacy csc compiles 0m to a Decimal.Zero
field load, which already decompiled to the literal, but Roslyn
compiles it to a zero-initialization, which decompiled to
default(decimal). The two forms are bit-identical for decimal, so the
literal is no less faithful to the IL and matches what a human writes;
it also removes the per-compiler split in the CompoundAssignmentTest
fixture.
Assisted-by: Claude:claude-fable-5:Claude Code
Operator precedence made the condition read
(setting && Add) || Subtract, so 'x -= 1' was converted to 'x--' even
with IntroduceIncrementAndDecrement disabled. Only observable with the
non-default setting, which no fixture configuration exercises, so no
test accompanies the fix.
Assisted-by: Claude:claude-fable-5:Claude Code
Reviving the 2020 test-cases-fp-types fixtures (compound assignment on
float, double and decimal) exposed an asymmetry: post-increment and
post-decrement on float/double round-tripped as x++/x--, but the pre
forms came back as x += 1f because the increment detection in
PrettifyAssignments only accepted integer constants. C# defines ++/--
on floating-point types as adding or subtracting exactly 1, so the
conversion is exact for a constant 1 operand. Decimal already works
through the op_Increment/op_Decrement path.
Assisted-by: Claude:claude-fable-5:Claude Code
Optimized code stores no temporary for a deconstruction element that is
used only once after the deconstruction. MatchAssignments handled that
for trailing elements, but a nested deconstruction copies the inner
element to a temporary, so the elements preceding it are also left
without an assignment; their external load then violated the
DeconstructInstruction invariant that all pattern variable loads are
descendants of the instruction. The forwarding fixup now covers all
unassigned elements and inserts in pattern order, because the statement
and expression builders pair pattern variables with assignments
positionally. This also fixes the nested tuple deconstruction crash
reported in #3388.
Also unwrap the address of the tested operand in
VisitDeconstructInstruction: deconstructing a struct passes the
receiver by reference, which was emitted as an invalid cast,
'var (x, y) = (S)(ref s);', even without nesting.
Fixes#3388.
Assisted-by: Claude:claude-fable-5:Claude Code
A class may name its own protected nested interface in its base list
(class F : F.IFoo), but referencing a protected interface nested in a
base class there (class SubF : F, F.IFoo) does not compile, even though
that interface is accessible inside the class body. The interface-impl
metadata still lists such interfaces (they are inherited through other
entries), so emitting every entry produced uncompilable sources. Skip
base types that are neither nested within the current type's nesting
chain nor accessible from the enclosing scope without the
protected-through-inheritance privilege. The check covers every type
the base-list reference names: type arguments, array and tuple
elements, and the declaring chain of the named type.
Assisted-by: Claude:claude-fable-5:Claude Code
The resolver comments cited section numbers from the C# 4.0 spec (and a
few from C# 9.0 drafts), which no longer match the published ECMA-334
standard. Renumber them against dotnet/csharpstandard draft-v11; every
reference was checked against the actual section headings. The old
'better conversion from type' subclause (7.5.3.4) no longer exists as
such and its rules live in 12.6.4.5-12.6.4.7, so that comment now says
so instead of pointing at a dead number.
Assisted-by: Claude:claude-fable-5:Claude Code
The .NET 10 BCL ships static [Extension] classes that contain ordinary
nested types (e.g. XDocumentExtensions.XDocumentNavigable). Decompiling
such a nested type's member in isolation resolved the enclosing
container's ExtensionInfo, and DecompileBody then dereferenced the
missing extension-member mapping. A container without any extension
blocks now reports no ExtensionInfo at all, and ResolveExtensionInfo
applies a container's info only to members that actually belong to one
of its extension blocks.
Assisted-by: Claude:claude-fable-5:Claude Code
* Make IProjectFileWriter implementations public and extensible
* Fix nullable error
* Fix delegate invocation to prevent race conditions
Refactored code to assign WriteCustomPropertyGroup and WriteCustomItemGroup delegates to local variables before null checks and invocation. This ensures thread safety by avoiding race conditions if the delegates are modified by other threads.
* Replace events with a GetCustomProperties virtual method
* Remove I prefix
The renamed-Implements case exposed a gap: the accessor-method
declarations did not include the explicit-interface-implementation
forwarders generated from .override directives, so decompiled types did
not implement their interfaces and same-name implementations lost their
interface mapping. DecompileParameterizedProperty now emits the same
forwarder stubs as the ordinary method path.
Assisted-by: Claude:claude-fable-5:Claude Code
The main tree, tooltips, and search results once showed the parameter
list of a parameterized property; the ambience lost that when property
rendering went through the converted AST node, whose C# property syntax
cannot carry parameters. Take the parameter list from the symbol
instead and render it in parentheses (matching VB.NET usage syntax and
distinguishing these properties from indexers).
Assisted-by: Claude:claude-fable-5:Claude Code
The compiler-generated documentation file contains a P: entry for a
parameterized property, but its accessors are emitted as ordinary
methods, whose M: id has no documentation entry. Fall back to the
owning property's documentation both when inserting XML documentation
into decompiled output (on the first accessor only, to avoid
duplicating it) and in the text view's tooltip (for either accessor).
Assisted-by: Claude:claude-fable-5:Claude Code
C# cannot declare a named property with parameters: only the type's
default member gets indexer syntax, and reusing it via [IndexerName]
collapses for types with several differently-named indexed properties,
static properties, or explicit interface implementations. Emitting the
accessors as ordinary methods is the only fully general compilable
form, matches how C# consumes such properties (Roslyn exposes the
accessors of properties it cannot bind as regular methods, the same
pattern C# 14 made user-facing for extension-member disambiguation),
and round-trips call sites to identical IL. Call sites already lower
to direct accessor calls.
The property-level attributes are kept on the first accessor under the
'property:' attribute target: it is not valid on methods, so csc emits
nothing for it (CS0657 warning) and recompilation neither loses the
attributes from the source nor misapplies them to the accessor. A
comment on the first accessor documents the deliberate deviation.
Visual Studio's metadata-as-source view drops such properties'
attributes entirely.
The assembly tree and tooltips are unaffected: they keep rendering the
property node with its parameter list. Known limitation, inherent to
any C# projection: recompiling the output produces plain methods, so
VB.NET consumers of the recompiled assembly lose property syntax.
Assisted-by: Claude:claude-fable-5:Claude Code
The foreach pattern was only matched against using instructions, but
the compiler emits no using/try-finally at all when the enumerator's
static type can never require disposal: a struct or a sealed class
that does not implement IDisposable (SerializationInfoEnumerator in
the issue's example). Such loops stayed while loops.
Recognize the bare 'enumerator = x.GetEnumerator(); while
(enumerator.MoveNext())' shape during statement building and reuse the
existing foreach transformation core for it. The transformation is
restricted to exactly the cases where recompilation would produce the
same IL: the enumerator type rules above (ref structs are excluded
because of pattern-based disposal), a single-store enumerator variable
unused outside the loop, and synchronous enumeration only, since async
enumerators are always IAsyncDisposable.
Assisted-by: Claude:claude-fable-5:Claude Code
allows ref struct is inherited implicitly, so restating it on an override is
CS0460 even alongside a legal disambiguator. Roslyn still re-emits the byreflike
flag on the override's own type parameter, and the general constraint printer
turns that flag back into source, so the disambiguator stays legal only as long
as it is built separately. Cover a C# 13 base whose annotated and plain methods
both allow ref structs.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
IsReferenceType is a bool?, so choosing between class, default and no
constraint at all is a three-state decision. Spelling those states out keeps
that visible where the choice is made, rather than leaving it implied by a
comparison against true.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
A class-type constraint such as Stream or Delegate sets no
ReferenceTypeConstraint flag, so keying the disambiguator off that flag gave
those overrides the default constraint, which is CS8822, and the output still
did not recompile. The restated disambiguator leaves no metadata trace of its
own, so the choice has to follow from whether the inherited constraints make
the type parameter a reference type, a value type, or neither.
Matching the annotated type parameters by identity rather than by owner kind
and index also keeps a specialized signature from contributing a foreign type
parameter that happens to share an index.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
Override constraints are normally inherited and omitted, but nullable type
parameters still require class or default to distinguish annotations from
Nullable<T>. Derive that legal discriminator from the method metadata.
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
Yield translation derived its target type only from synchronous enumerable
interfaces, leaving async iterator yields untyped and preserving compiler
boxing casts. Use the element type already recovered by the async decompiler.
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
ExpressionBuilder printed bitwise & / | on booleans as && / || whenever
the right-hand side was pure, but Roslyn lowers && / || to & / | only
when the right operand is a bare local or parameter read
(LocalRewriter.MakeBinaryOperator, unchanged since 2014). Shapes like
(c == 'a') | (c == 'b') can therefore only originate from a bitwise
source operator, yet were shown as short-circuiting. Per the discussion
in #1545, show the operator the IL actually uses instead of guessing the
source form: the reversal is dropped entirely, so Roslyn-compiled
"a && b" now decompiles to "a & b", which recompiles to the same IL.
Assisted-by: Claude:claude-fable-5:Claude Code
A dynamic index access (a[b]) gave its IndexerExpression a
DynamicInvocationResolveResult with no symbol, so the brackets carried no
tooltip. Synthesize an indexer (FakeProperty, IsIndexer) on the target
type with the index parameters typed from the callsite delegate, and
attach it. Route it hover-only by detecting a DynamicInvocationResolveResult
directly on the node - which also covers an invoke-member's own
parentheses, so those stop producing a dead navigation link too.
Assisted-by: Claude:claude-fable-5:Claude Code
A dynamic invocation with explicit type arguments (a.Method<int>())
synthesized a non-generic fake method, so the hover showed Method(...)
without the generic list. Give the fake a matching set of conventionally
named type parameters (T, T2, ...) and specialize it with the actual
arguments, as a real generic call produces a SpecializedMethod, so the
hover shows the method's generic parameters.
Assisted-by: Claude:claude-fable-5:Claude Code
A dynamic object creation (new T(b) where an argument is dynamic) gave
its ObjectCreateExpression only a plain ResolveResult(T), so neither the
type name nor the parentheses referenced a constructor - unlike a normal
new expression, whose CSharpInvocationResolveResult lets both hover the
ctor. Synthesize a constructor on the created type (parameters typed from
the callsite delegate) and attach it the same way. Since it has no
metadata, route it hover-only, like the other dynamic members, so the
type name's existing navigation is not replaced by a dead link.
Assisted-by: Claude:claude-fable-5:Claude Code
Dynamic member accesses and invocations carried only the member name
(DynamicMemberResolveResult / DynamicInvocationResolveResult), so
GetSymbol returned null and the editor emitted no reference or hover.
Synthesize a member on the target type - a dynamic field for a member
access, a dynamic-returning method for a member invocation - named after
the accessed member and typed from the callsite delegate: each argument
uses its recorded compile-time type when the binder set one (statically
typed or constant arguments), dynamic otherwise, and the declaring type
comes from the receiver's argument info. Route these through GetSymbol;
TextTokenWriter and the hover renderer already turn an IEntity into a
tooltip. The synthesized members have no metadata token, so they render
a signature on hover but are not navigation targets.
Assisted-by: Claude:claude-fable-5:Claude Code
The events introduced in #2519 timed only the five per-entity DoDecompile
overloads, allocated a Stopwatch and the member's FullName even when no
trace session was attached, and reported whole milliseconds, which rounds
almost every member to zero. Flat one-shot events also gave PerfView and
dotnet-trace no way to show durations or nesting, and the actually
expensive stages (type system initialization, assembly resolution probing,
the IL/AST transform pipelines, whole-project decompilation) were not
instrumented at all.
Start/Stop event pairs let trace viewers derive duration and nesting from
event timestamps, keywords let a session enable only the areas of
interest, and every call site is gated on IsEnabled() so tracing costs a
branch when disabled. Per-transform events are Verbose because of their
volume; unlike the STEP/Stepper mechanism they work in Release builds.
EventSource is in-box for netstandard2.0 and flows over both ETW and
EventPipe, so this stays cross-platform with no new dependency.
Assisted-by: Claude:claude-fable-5:Claude Code
Enum members whose value duplicates an earlier member now reference it
(Item2B = Item2A), [Flags] members combine earlier single-bit members
(All = Item1 | Item2 | Item3) or their complement (NotItem1 = ~Item1)
instead of showing a bare number.
Several guardrails keep the output faithful to how such enums are
written by hand: only previously declared members are referenced (field
row order); a multi-bit value lying entirely within a larger, earlier
member is a field encoding inside that mask, not a flag union, and
stays numeric, as do zero-valued members of [Flags] enums, which
routinely have several unrelated zero members. The ~X form is
suppressed in byte/ushort enum declarations, where the initializer
constant folds in int and would not compile. Enums with unusual
underlying types (bool, native int) keep the plain constant conversion.
With these rules, decompiling System.Private.CoreLib reproduces the
hand-written declarations of TypeAttributes, MethodAttributes,
AttributeTargets and FileAttributes almost verbatim.
Assisted-by: Claude:claude-fable-5:Claude Code
Object creation can require an unsafe context solely because the selected
constructor has a pointer parameter. Apply the existing unsafe-signature
check to object creation nodes so the emitted declaration remains compilable.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
With recognition on the ILAst and reference substitution during
translation, the four accessor-body patterns in
PatternStatementTransform were only reachable as a fallback, and any
divergence between them and the AutoEventDecompiler verdict produced
inconsistent output. A compiler shape the ILAst matchers do not know now
degrades to explicit accessors with the backing field kept in the
output, which stays compilable. Bodyless events (abstract, extern,
interface members) previously relied on the patterns' no-body clause to
become field-like; since C# cannot express bodyless custom accessors,
DoDecompile now chooses the field-like form for them directly. Also
deletes the orphaned IsEventBackingFieldName helper; the name
association lives in PropertyAndEventBackingFieldLookup.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
PatternStatementTransform renamed backing-field identifiers to the event
after the fact, keyed on the metadata name association alone: references
belonging to an event that is not actually automatic were still renamed,
binding them to a custom event that is unusable as a value (the same
defect class as #3858), and the rename bypassed the resolver checks, so
qualifiers were computed for the hidden field instead of the printed
event. ExpressionBuilder.ConvertField now performs the substitution,
keyed on the AutoEventDecompiler verdict whose memo moves into
DecompileRun so that member hiding, the event declaration, and reference
translation all decide from one analysis. Checking the verdict's field
identity also keeps same-typed sibling events apart (#3575), and the
qualifier logic running against the event drops spurious this./type
qualifiers from raise sites.
mcs 2.x accesses a sibling automatic event's backing field directly
inside custom accessors instead of calling the accessor, so the fixture
expects the resulting Delegate.Combine form there.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
MemberIsHidden decides field hiding from the metadata name association
alone, which over-approximates: an event whose accessors fail the ILAst
validation is decompiled with explicit accessors, and while a referenced
backing field is re-added through the work list, an unreferenced one was
silently dropped from the output. The type-definition member loop now
consults the same memoized verdict as the event declaration, so the two
decisions agree by construction.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
The syntactic accessor-body patterns in PatternStatementTransform sit
downstream of every settings-dependent transform, so each new compiler
shape or settings combination silently broke recognition: with
AggressiveInlining enabled, static events inline the Delegate.Combine
call into CompareExchange positionally, which none of the four patterns
matched, while call sites were still rewritten to the event name from
metadata alone - producing uncompilable output (CS0079).
Recognition now happens in DoDecompile(IEvent) by structurally matching
the ILAst of the accessors, decompiled with a fixed set of settings the
same way RecordDecompiler analyzes method bodies. This makes detection
independent of the user-visible settings by construction. Events that
are not recognized fall back to the classic path unchanged, including
the existing AST patterns.
mcs 2.x compiles the accessors as a compound assignment, evaluating
'this' once via IL 'dup'; the simple-combine matcher accepts that
stack-slot alias.
Assisted-by: Claude:claude-fable-5:Claude Code
Structural recognition of compiler-generated code needs method bodies as
ILAst decompiled with a fixed set of settings, so that recognition does
not depend on user-visible options. RecordDecompiler had this pipeline
as a private helper; hoist it to CSharpDecompiler so other recognizers
can share it, deriving the generic context from the method's declaring
type instead of a captured type definition.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
Roslyn caches a ReadOnlySpan<T> created from an array literal in a
<PrivateImplementationDetails> field on target frameworks without
RuntimeHelpers.CreateSpan (e.g. .NET Framework / netstandard2.0 + System.Memory):
object obj = <PrivateImplementationDetails>.cache;
if (obj == null) {
obj = new char[] { '\r', '\n' };
<PrivateImplementationDetails>.cache = (char[])obj;
}
... new ReadOnlySpan<char>((char[])obj) ...
The decompiled output referenced the compiler-synthesized
<PrivateImplementationDetails> type, whose escaped name is not expressible in C#
and is never declared, so the output failed to recompile (CS0400).
The modern RuntimeHelpers.CreateSpan form was already handled
(TransformRuntimeHelpersCreateSpanInitialization); this adds the analogous
handling for the legacy lazy-cache form, mirroring CachedDelegateInitialization
(which collapses the same lazy-static-field cache for anonymous-method delegates).
Once the cache is collapsed, the existing array-initializer transforms recover
the array literal, so the <PrivateImplementationDetails> reference disappears.
Test: ILPretty/CachedReadOnlySpanInitialization.
When a null literal is the only argument a generic type argument could
be inferred from, and that type argument is an anonymous type, ILSpy
used to drop the type arguments entirely (they cannot be written
explicitly), producing 'Test(null)', which no longer compiles
(CS0411). A conditional expression whose never-taken branch creates an
instance of the anonymous type is the minimal C# expression that gives
a null value that type, so the argument now carries enough information
for type inference.
The instance expression is obtained by translating a synthesized
'newobj' with 'default.value' arguments through the existing pipeline
rather than assembling syntax by hand; the property values are typed
defaults, since the IL only contains ldnull. Anonymous types occurring
inside other constructed types (e.g. arrays) stay unexpressible and
keep the previous output.
Assisted-by: Claude:claude-fable-5:Claude Code
Passing the same local as multiple out arguments of one call made
DeclareVariables turn the first use into an implicitly-typed declaration,
producing 'f(out var x, out x)'. Referencing an implicitly-typed out
variable in another argument of the declaring call is rejected by the
compiler (CS8196), because its type is only inferred once overload
resolution of that call has completed. The explicitly-typed form
'f(out int x, out x)' is valid, so fall back to the explicit type in
that case.
Assisted-by: Claude:claude-fable-5:Claude Code
Backfills the standard MIT X11 header on hand-written files that never
got one, attributing each to its first-commit author and year from git
history. Code vendored from dotnet/runtime and Humanizr/Humanizer gets
its origin's license lines and a provenance note instead. Generated
files (Resources.Designer.cs, the version-info template), tool-managed
suppression files, and BAML test-case fixtures intentionally stay
header-less.
Assisted-by: Claude:claude-fable-5:Claude Code
The C# compiler lowers a foreach over an inline array into a for loop whose
body reads each element through <PrivateImplementationDetails>.InlineArrayElementRef(ref
buffer, i). That helper cannot be named in C#, so the decompiled for loop did
not compile.
Reconstruct the foreach at the AST level instead. Rewriting the unchecked
InlineArrayElementRef helper to the bounds-checked indexer buffer[i] would be
unsound for an out-of-bounds index, so the transform fires only when the loop
bound equals the inline array's length: that proves 0 <= i < length, matching
the exact shape the compiler emits and nothing else. A loop that does not match
keeps the faithful (if unnameable) helper call rather than silently gaining a
bounds check.
Assisted-by: Claude:claude-fable-5:Claude Code
Per @siegfriedpammer: rather than rescue the hoisted for-initializer in
DeclareVariables, don't form the for-loop at all. TransformFor now bails when the
loop variable is a by-ref-like local used after the loop, leaving the while-loop
(matching source); the ref decl keeps its initializer, no CS8174. Reverts the
DeclareVariables change; test now expects the while form.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
A ref local that is used after a for-loop has its declaration hoisted in front of
the loop, but its only initialization is the for-initializer ref-assignment. The
declaration was then emitted without an initializer (`ref T x;`), which does not
compile (CS8174).
When a by-ref-like local's matching assignment is the first for-initializer, move
the ref-assignment's value up into the declaration (`ref T x = ref expr;`) and
drop the for-initializer.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
A type's leading field assignments are extracted to field declarations
only when every constructor that does not chain with this() agrees on
them. When they disagree, the analyzer gave up on the whole type, so the
remaining constructors' this()/base() calls were never lifted into
initializers -- a struct with two divergent constructors plus a chaining
one rendered the chain as `this = new TSelf(...)` instead of `: this(...)`.
Chain lifting does not depend on the shared-initializer extraction, so a
mismatch now just skips the extraction (the assignments stay in the
bodies) and the transform continues. Primary constructors still bail,
since their parameters drive the initializers that must be extracted.
Assisted-by: Claude:claude-opus-4-8:Claude Code